Merge branch 'main' into feat/folders
This commit is contained in:
19
test/admin/collections/Array.ts
Normal file
19
test/admin/collections/Array.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { arrayCollectionSlug } from '../slugs.js'
|
||||
|
||||
export const Array: CollectionConfig = {
|
||||
slug: arrayCollectionSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
41
test/admin/collections/Placeholder.ts
Normal file
41
test/admin/collections/Placeholder.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { placeholderCollectionSlug } from '../slugs.js'
|
||||
|
||||
export const Placeholder: CollectionConfig = {
|
||||
slug: placeholderCollectionSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'defaultSelect',
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: 'option1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'placeholderSelect',
|
||||
type: 'select',
|
||||
options: [{ label: 'Option 1', value: 'option1' }],
|
||||
admin: {
|
||||
placeholder: 'Custom placeholder',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'defaultRelationship',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
{
|
||||
name: 'placeholderRelationship',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
admin: {
|
||||
placeholder: 'Custom placeholder',
|
||||
},
|
||||
},
|
||||
],
|
||||
versions: true,
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { Array } from './collections/Array.js'
|
||||
import { BaseListFilter } from './collections/BaseListFilter.js'
|
||||
import { CustomFields } from './collections/CustomFields/index.js'
|
||||
import { CustomViews1 } from './collections/CustomViews1.js'
|
||||
@@ -18,6 +18,7 @@ import { CollectionHidden } from './collections/Hidden.js'
|
||||
import { ListDrawer } from './collections/ListDrawer.js'
|
||||
import { CollectionNoApiView } from './collections/NoApiView.js'
|
||||
import { CollectionNotInView } from './collections/NotInView.js'
|
||||
import { Placeholder } from './collections/Placeholder.js'
|
||||
import { Posts } from './collections/Posts.js'
|
||||
import { UploadCollection } from './collections/Upload.js'
|
||||
import { UploadTwoCollection } from './collections/UploadTwo.js'
|
||||
@@ -42,7 +43,8 @@ import {
|
||||
protectedCustomNestedViewPath,
|
||||
publicCustomViewPath,
|
||||
} from './shared.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
export default buildConfigWithDefaults({
|
||||
admin: {
|
||||
importMap: {
|
||||
@@ -158,11 +160,13 @@ export default buildConfigWithDefaults({
|
||||
CollectionGroup2A,
|
||||
CollectionGroup2B,
|
||||
Geo,
|
||||
Array,
|
||||
DisableDuplicate,
|
||||
DisableCopyToLocale,
|
||||
BaseListFilter,
|
||||
with300Documents,
|
||||
ListDrawer,
|
||||
Placeholder,
|
||||
],
|
||||
globals: [
|
||||
GlobalHidden,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import type { User as PayloadUser } from 'payload'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { mapAsync } from 'payload'
|
||||
import * as qs from 'qs-esm'
|
||||
|
||||
import type { Config, Geo, Post, User } from '../../payload-types.js'
|
||||
import type { Config, Geo, Post } from '../../payload-types.js'
|
||||
|
||||
import {
|
||||
ensureCompilationIsDone,
|
||||
@@ -17,9 +16,11 @@ import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||
import { customAdminRoutes } from '../../shared.js'
|
||||
import {
|
||||
arrayCollectionSlug,
|
||||
customViews1CollectionSlug,
|
||||
geoCollectionSlug,
|
||||
listDrawerSlug,
|
||||
placeholderCollectionSlug,
|
||||
postsCollectionSlug,
|
||||
with300DocumentsSlug,
|
||||
} from '../../slugs.js'
|
||||
@@ -57,11 +58,13 @@ const dirname = path.resolve(currentFolder, '../../')
|
||||
describe('List View', () => {
|
||||
let page: Page
|
||||
let geoUrl: AdminUrlUtil
|
||||
let arrayUrl: AdminUrlUtil
|
||||
let postsUrl: AdminUrlUtil
|
||||
let baseListFiltersUrl: AdminUrlUtil
|
||||
let customViewsUrl: AdminUrlUtil
|
||||
let with300DocumentsUrl: AdminUrlUtil
|
||||
let withListViewUrl: AdminUrlUtil
|
||||
let placeholderUrl: AdminUrlUtil
|
||||
let user: any
|
||||
|
||||
let serverURL: string
|
||||
@@ -79,12 +82,13 @@ describe('List View', () => {
|
||||
}))
|
||||
|
||||
geoUrl = new AdminUrlUtil(serverURL, geoCollectionSlug)
|
||||
arrayUrl = new AdminUrlUtil(serverURL, arrayCollectionSlug)
|
||||
postsUrl = new AdminUrlUtil(serverURL, postsCollectionSlug)
|
||||
with300DocumentsUrl = new AdminUrlUtil(serverURL, with300DocumentsSlug)
|
||||
baseListFiltersUrl = new AdminUrlUtil(serverURL, 'base-list-filters')
|
||||
customViewsUrl = new AdminUrlUtil(serverURL, customViews1CollectionSlug)
|
||||
withListViewUrl = new AdminUrlUtil(serverURL, listDrawerSlug)
|
||||
|
||||
placeholderUrl = new AdminUrlUtil(serverURL, placeholderCollectionSlug)
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
initPageConsoleErrorCatch(page)
|
||||
@@ -389,6 +393,32 @@ describe('List View', () => {
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(2)
|
||||
})
|
||||
|
||||
test('should allow to filter in array field', async () => {
|
||||
await createArray()
|
||||
|
||||
await page.goto(arrayUrl.list)
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(1)
|
||||
|
||||
await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Array > Text',
|
||||
operatorLabel: 'equals',
|
||||
value: 'test',
|
||||
})
|
||||
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(1)
|
||||
|
||||
await page.locator('.condition__actions .btn.condition__actions-remove').click()
|
||||
await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Array > Text',
|
||||
operatorLabel: 'equals',
|
||||
value: 'not-matching',
|
||||
})
|
||||
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('should reset filter value when a different field is selected', async () => {
|
||||
const id = (await page.locator('.cell-id').first().innerText()).replace('ID: ', '')
|
||||
|
||||
@@ -1379,6 +1409,66 @@ describe('List View', () => {
|
||||
).toHaveText('Title')
|
||||
})
|
||||
})
|
||||
|
||||
describe('placeholder', () => {
|
||||
test('should display placeholder in filter options', async () => {
|
||||
await page.goto(
|
||||
`${placeholderUrl.list}${qs.stringify(
|
||||
{
|
||||
where: {
|
||||
or: [
|
||||
{
|
||||
and: [
|
||||
{
|
||||
defaultSelect: {
|
||||
equals: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
placeholderSelect: {
|
||||
equals: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
defaultRelationship: {
|
||||
equals: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
placeholderRelationship: {
|
||||
equals: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{ addQueryPrefix: true },
|
||||
)}`,
|
||||
)
|
||||
|
||||
const conditionValueSelects = page.locator('#list-controls-where .condition__value')
|
||||
await expect(conditionValueSelects.nth(0)).toHaveText('Select a value')
|
||||
await expect(conditionValueSelects.nth(1)).toHaveText('Custom placeholder')
|
||||
await expect(conditionValueSelects.nth(2)).toHaveText('Select a value')
|
||||
await expect(conditionValueSelects.nth(3)).toHaveText('Custom placeholder')
|
||||
})
|
||||
})
|
||||
test('should display placeholder in edit view', async () => {
|
||||
await page.goto(placeholderUrl.create)
|
||||
|
||||
await expect(page.locator('#field-defaultSelect .rs__placeholder')).toHaveText('Select a value')
|
||||
await expect(page.locator('#field-placeholderSelect .rs__placeholder')).toHaveText(
|
||||
'Custom placeholder',
|
||||
)
|
||||
await expect(page.locator('#field-defaultRelationship .rs__placeholder')).toHaveText(
|
||||
'Select a value',
|
||||
)
|
||||
await expect(page.locator('#field-placeholderRelationship .rs__placeholder')).toHaveText(
|
||||
'Custom placeholder',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
async function createPost(overrides?: Partial<Post>): Promise<Post> {
|
||||
@@ -1405,3 +1495,12 @@ async function createGeo(overrides?: Partial<Geo>): Promise<Geo> {
|
||||
},
|
||||
}) as unknown as Promise<Geo>
|
||||
}
|
||||
|
||||
async function createArray() {
|
||||
return payload.create({
|
||||
collection: arrayCollectionSlug,
|
||||
data: {
|
||||
array: [{ text: 'test' }],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ export interface Config {
|
||||
'group-two-collection-ones': GroupTwoCollectionOne;
|
||||
'group-two-collection-twos': GroupTwoCollectionTwo;
|
||||
geo: Geo;
|
||||
array: Array;
|
||||
'disable-duplicate': DisableDuplicate;
|
||||
'disable-copy-to-locale': DisableCopyToLocale;
|
||||
'base-list-filters': BaseListFilter;
|
||||
@@ -108,6 +109,7 @@ export interface Config {
|
||||
'group-two-collection-ones': GroupTwoCollectionOnesSelect<false> | GroupTwoCollectionOnesSelect<true>;
|
||||
'group-two-collection-twos': GroupTwoCollectionTwosSelect<false> | GroupTwoCollectionTwosSelect<true>;
|
||||
geo: GeoSelect<false> | GeoSelect<true>;
|
||||
array: ArraySelect<false> | ArraySelect<true>;
|
||||
'disable-duplicate': DisableDuplicateSelect<false> | DisableDuplicateSelect<true>;
|
||||
'disable-copy-to-locale': DisableCopyToLocaleSelect<false> | DisableCopyToLocaleSelect<true>;
|
||||
'base-list-filters': BaseListFiltersSelect<false> | BaseListFiltersSelect<true>;
|
||||
@@ -414,6 +416,21 @@ export interface Geo {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "array".
|
||||
*/
|
||||
export interface Array {
|
||||
id: string;
|
||||
array?:
|
||||
| {
|
||||
text?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "disable-duplicate".
|
||||
@@ -534,6 +551,10 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'geo';
|
||||
value: string | Geo;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'array';
|
||||
value: string | Array;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'disable-duplicate';
|
||||
value: string | DisableDuplicate;
|
||||
@@ -818,6 +839,20 @@ export interface GeoSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "array_select".
|
||||
*/
|
||||
export interface ArraySelect<T extends boolean = true> {
|
||||
array?:
|
||||
| T
|
||||
| {
|
||||
text?: T;
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "disable-duplicate_select".
|
||||
|
||||
@@ -2,6 +2,7 @@ export const usersCollectionSlug = 'users'
|
||||
export const customViews1CollectionSlug = 'custom-views-one'
|
||||
export const customViews2CollectionSlug = 'custom-views-two'
|
||||
export const geoCollectionSlug = 'geo'
|
||||
export const arrayCollectionSlug = 'array'
|
||||
export const postsCollectionSlug = 'posts'
|
||||
export const group1Collection1Slug = 'group-one-collection-ones'
|
||||
export const group1Collection2Slug = 'group-one-collection-twos'
|
||||
@@ -13,6 +14,7 @@ export const noApiViewCollectionSlug = 'collection-no-api-view'
|
||||
export const disableDuplicateSlug = 'disable-duplicate'
|
||||
export const disableCopyToLocale = 'disable-copy-to-locale'
|
||||
export const uploadCollectionSlug = 'uploads'
|
||||
export const placeholderCollectionSlug = 'placeholder'
|
||||
|
||||
export const uploadTwoCollectionSlug = 'uploads-two'
|
||||
export const customFieldsSlug = 'custom-fields'
|
||||
@@ -23,6 +25,7 @@ export const collectionSlugs = [
|
||||
customViews1CollectionSlug,
|
||||
customViews2CollectionSlug,
|
||||
geoCollectionSlug,
|
||||
arrayCollectionSlug,
|
||||
postsCollectionSlug,
|
||||
group1Collection1Slug,
|
||||
group1Collection2Slug,
|
||||
|
||||
51
test/auth/forgot-password-localized/config.ts
Normal file
51
test/auth/forgot-password-localized/config.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import { buildConfigWithDefaults } from '../../buildConfigWithDefaults.js'
|
||||
|
||||
export const collectionSlug = 'users'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
admin: {
|
||||
user: collectionSlug,
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
localization: {
|
||||
locales: ['en', 'pl'],
|
||||
defaultLocale: 'en',
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
slug: collectionSlug,
|
||||
auth: {
|
||||
forgotPassword: {
|
||||
// Default options
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'localizedField',
|
||||
type: 'text',
|
||||
localized: true, // This field is localized and will require locale during validation
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'roles',
|
||||
type: 'select',
|
||||
defaultValue: ['user'],
|
||||
hasMany: true,
|
||||
label: 'Role',
|
||||
options: ['admin', 'editor', 'moderator', 'user', 'viewer'],
|
||||
required: true,
|
||||
saveToJWT: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
debug: true,
|
||||
})
|
||||
78
test/auth/forgot-password-localized/int.spec.ts
Normal file
78
test/auth/forgot-password-localized/int.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { NextRESTClient } from '../../helpers/NextRESTClient.js'
|
||||
|
||||
import { devUser } from '../../credentials.js'
|
||||
import { initPayloadInt } from '../../helpers/initPayloadInt.js'
|
||||
import { collectionSlug } from './config.js'
|
||||
|
||||
let restClient: NextRESTClient | undefined
|
||||
let payload: Payload | undefined
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
describe('Forgot password operation with localized fields', () => {
|
||||
beforeAll(async () => {
|
||||
;({ payload, restClient } = await initPayloadInt(dirname, 'auth/forgot-password-localized'))
|
||||
|
||||
// Register a user with additional localized field
|
||||
const res = await restClient?.POST(`/${collectionSlug}/first-register?locale=en`, {
|
||||
body: JSON.stringify({
|
||||
...devUser,
|
||||
'confirm-password': devUser.password,
|
||||
localizedField: 'English content',
|
||||
}),
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
throw new Error('Failed to register user')
|
||||
}
|
||||
|
||||
const { user } = await res.json()
|
||||
|
||||
// @ts-expect-error - Localized field is not in the general Payload type, but it is in mocked collection in this case.
|
||||
await payload?.update({
|
||||
collection: collectionSlug,
|
||||
id: user.id as string,
|
||||
locale: 'pl',
|
||||
data: {
|
||||
localizedField: 'Polish content',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (typeof payload?.db.destroy === 'function') {
|
||||
await payload?.db.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
it('should successfully process forgotPassword operation with localized fields', async () => {
|
||||
// Attempt to trigger forgotPassword operation
|
||||
const token = await payload?.forgotPassword({
|
||||
collection: collectionSlug,
|
||||
data: { email: devUser.email },
|
||||
disableEmail: true,
|
||||
})
|
||||
|
||||
// Verify token was generated successfully
|
||||
expect(token).toBeDefined()
|
||||
expect(typeof token).toBe('string')
|
||||
expect(token?.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should not throw validation errors for localized fields', async () => {
|
||||
// We expect this not to throw an error
|
||||
await expect(
|
||||
payload?.forgotPassword({
|
||||
collection: collectionSlug,
|
||||
data: { email: devUser.email },
|
||||
disableEmail: true,
|
||||
}),
|
||||
).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
246
test/auth/forgot-password-localized/payload-types.ts
Normal file
246
test/auth/forgot-password-localized/payload-types.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| 'Pacific/Midway'
|
||||
| 'Pacific/Niue'
|
||||
| 'Pacific/Honolulu'
|
||||
| 'Pacific/Rarotonga'
|
||||
| 'America/Anchorage'
|
||||
| 'Pacific/Gambier'
|
||||
| 'America/Los_Angeles'
|
||||
| 'America/Tijuana'
|
||||
| 'America/Denver'
|
||||
| 'America/Phoenix'
|
||||
| 'America/Chicago'
|
||||
| 'America/Guatemala'
|
||||
| 'America/New_York'
|
||||
| 'America/Bogota'
|
||||
| 'America/Caracas'
|
||||
| 'America/Santiago'
|
||||
| 'America/Buenos_Aires'
|
||||
| 'America/Sao_Paulo'
|
||||
| 'Atlantic/South_Georgia'
|
||||
| 'Atlantic/Azores'
|
||||
| 'Atlantic/Cape_Verde'
|
||||
| 'Europe/London'
|
||||
| 'Europe/Berlin'
|
||||
| 'Africa/Lagos'
|
||||
| 'Europe/Athens'
|
||||
| 'Africa/Cairo'
|
||||
| 'Europe/Moscow'
|
||||
| 'Asia/Riyadh'
|
||||
| 'Asia/Dubai'
|
||||
| 'Asia/Baku'
|
||||
| 'Asia/Karachi'
|
||||
| 'Asia/Tashkent'
|
||||
| 'Asia/Calcutta'
|
||||
| 'Asia/Dhaka'
|
||||
| 'Asia/Almaty'
|
||||
| 'Asia/Jakarta'
|
||||
| 'Asia/Bangkok'
|
||||
| 'Asia/Shanghai'
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
| 'Pacific/Auckland'
|
||||
| 'Pacific/Fiji';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: 'en' | 'pl';
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
localizedField: string;
|
||||
roles: ('admin' | 'editor' | 'moderator' | 'user' | 'viewer')[];
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null;
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
localizedField?: T;
|
||||
roles?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
@@ -1016,6 +1016,7 @@ describe('Auth', () => {
|
||||
expect(emailValidation('user.name+alias@example.co.uk', mockContext)).toBe(true)
|
||||
expect(emailValidation('user-name@example.org', mockContext)).toBe(true)
|
||||
expect(emailValidation('user@ex--ample.com', mockContext)).toBe(true)
|
||||
expect(emailValidation("user'payload@example.org", mockContext)).toBe(true)
|
||||
})
|
||||
|
||||
it('should not allow emails with double quotes', () => {
|
||||
@@ -1045,5 +1046,11 @@ describe('Auth', () => {
|
||||
expect(emailValidation('user@-example.com', mockContext)).toBe('validation:emailAddress')
|
||||
expect(emailValidation('user@example-.com', mockContext)).toBe('validation:emailAddress')
|
||||
})
|
||||
it('should not allow emails that start with dot', () => {
|
||||
expect(emailValidation('.user@example.com', mockContext)).toBe('validation:emailAddress')
|
||||
})
|
||||
it('should not allow emails that have a comma', () => {
|
||||
expect(emailValidation('user,name@example.com', mockContext)).toBe('validation:emailAddress')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1997,6 +1997,23 @@ describe('database', () => {
|
||||
expect(draft.docs[0]?.postTitle).toBe('my-title')
|
||||
})
|
||||
|
||||
it('should not break when using select', async () => {
|
||||
const post = await payload.create({ collection: 'posts', data: { title: 'my-title-10' } })
|
||||
const { id } = await payload.create({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
data: { post: post.id },
|
||||
})
|
||||
|
||||
const doc = await payload.findByID({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
id,
|
||||
select: { postTitle: true },
|
||||
})
|
||||
expect(doc.postTitle).toBe('my-title-10')
|
||||
})
|
||||
|
||||
it('should allow virtual field as reference to ID', async () => {
|
||||
const post = await payload.create({ collection: 'posts', data: { title: 'my-title' } })
|
||||
const { id } = await payload.create({
|
||||
@@ -2129,6 +2146,26 @@ describe('database', () => {
|
||||
expect(doc.postCategoryTitle).toBe('1-category')
|
||||
})
|
||||
|
||||
it('should not break when using select 2x deep', async () => {
|
||||
const category = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: '3-category' },
|
||||
})
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: { title: '3-post', category: category.id },
|
||||
})
|
||||
const doc = await payload.create({ collection: 'virtual-relations', data: { post: post.id } })
|
||||
|
||||
const docWithSelect = await payload.findByID({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
id: doc.id,
|
||||
select: { postCategoryTitle: true },
|
||||
})
|
||||
expect(docWithSelect.postCategoryTitle).toBe('3-category')
|
||||
})
|
||||
|
||||
it('should allow to query by virtual field 2x deep', async () => {
|
||||
const category = await payload.create({
|
||||
collection: 'categories',
|
||||
@@ -2414,4 +2451,37 @@ describe('database', () => {
|
||||
|
||||
expect(res.docs[0].id).toBe(customID.id)
|
||||
})
|
||||
|
||||
it('should count with a query that contains subqueries', async () => {
|
||||
const category = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'new-category' },
|
||||
})
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: { title: 'new-post', category: category.id },
|
||||
})
|
||||
|
||||
const result_1 = await payload.count({
|
||||
collection: 'posts',
|
||||
where: {
|
||||
'category.title': {
|
||||
equals: 'new-category',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result_1.totalDocs).toBe(1)
|
||||
|
||||
const result_2 = await payload.count({
|
||||
collection: 'posts',
|
||||
where: {
|
||||
'category.title': {
|
||||
equals: 'non-existing-category',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result_2.totalDocs).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -358,6 +358,46 @@ describe('relationship', () => {
|
||||
).toHaveText(`${value}123456`)
|
||||
})
|
||||
|
||||
test('should open related document in a new tab when meta key is applied', async () => {
|
||||
await page.goto(url.create)
|
||||
|
||||
const [newPage] = await Promise.all([
|
||||
page.context().waitForEvent('page'),
|
||||
await openDocDrawer({
|
||||
page,
|
||||
selector:
|
||||
'#field-relationWithAllowCreateToFalse .relationship--single-value__drawer-toggler',
|
||||
withMetaKey: true,
|
||||
}),
|
||||
])
|
||||
|
||||
// Wait for navigation to complete in the new tab and ensure the edit view is open
|
||||
await expect(newPage.locator('.collection-edit')).toBeVisible()
|
||||
})
|
||||
|
||||
test('multi value relationship should open document in a new tab', async () => {
|
||||
await page.goto(url.create)
|
||||
|
||||
// Select "Seeded text document" relationship
|
||||
await page.locator('#field-relationshipHasMany .rs__control').click()
|
||||
await page.locator('.rs__option:has-text("Seeded text document")').click()
|
||||
await expect(
|
||||
page.locator('#field-relationshipHasMany .relationship--multi-value-label__drawer-toggler'),
|
||||
).toBeVisible()
|
||||
|
||||
const [newPage] = await Promise.all([
|
||||
page.context().waitForEvent('page'),
|
||||
await openDocDrawer({
|
||||
page,
|
||||
selector: '#field-relationshipHasMany .relationship--multi-value-label__drawer-toggler',
|
||||
withMetaKey: true,
|
||||
}),
|
||||
])
|
||||
|
||||
// Wait for navigation to complete in the new tab and ensure the edit view is open
|
||||
await expect(newPage.locator('.collection-edit')).toBeVisible()
|
||||
})
|
||||
|
||||
// Drawers opened through the edit button are prone to issues due to the use of stopPropagation for certain
|
||||
// events - specifically for drawers opened through the edit button. This test is to ensure that drawers
|
||||
// opened through the edit button can be saved using the hotkey.
|
||||
@@ -742,7 +782,15 @@ describe('relationship', () => {
|
||||
await expect(listDrawerContent).toBeHidden()
|
||||
|
||||
const selectedValue = relationshipField.locator('.relationship--multi-value-label__text')
|
||||
await expect(selectedValue).toBeVisible()
|
||||
await expect(selectedValue).toHaveCount(1)
|
||||
|
||||
await relationshipField.click()
|
||||
await expect(listDrawerContent).toBeVisible()
|
||||
await button.click()
|
||||
await expect(listDrawerContent).toBeHidden()
|
||||
|
||||
const selectedValues = relationshipField.locator('.relationship--multi-value-label__text')
|
||||
await expect(selectedValues).toHaveCount(2)
|
||||
})
|
||||
|
||||
test('should handle `hasMany` polymorphic relationship when `appearance: "drawer"`', async () => {
|
||||
@@ -807,6 +855,42 @@ describe('relationship', () => {
|
||||
await expect(newRows).toHaveCount(1)
|
||||
await expect(listDrawerContent.getByText('Seeded text document')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('should filter out existing values from polymorphic relationship list drawer', async () => {
|
||||
await page.goto(url.create)
|
||||
const relationshipField = page.locator('#field-polymorphicRelationshipDrawer')
|
||||
await relationshipField.click()
|
||||
const listDrawerContent = page.locator('.list-drawer').locator('.drawer__content')
|
||||
await expect(listDrawerContent).toBeVisible()
|
||||
|
||||
const relationToSelector = page.locator('.list-header__select-collection')
|
||||
await expect(relationToSelector).toBeVisible()
|
||||
|
||||
await relationToSelector.locator('.rs__control').click()
|
||||
const option = relationToSelector.locator('.rs__option').nth(1)
|
||||
await option.click()
|
||||
const rows = listDrawerContent.locator('table tbody tr')
|
||||
await expect(rows).toHaveCount(2)
|
||||
const firstRow = rows.first()
|
||||
const button = firstRow.locator('button')
|
||||
await button.click()
|
||||
await expect(listDrawerContent).toBeHidden()
|
||||
|
||||
const selectedValue = relationshipField.locator('.relationship--single-value__text')
|
||||
await expect(selectedValue).toBeVisible()
|
||||
|
||||
await relationshipField.click()
|
||||
await expect(listDrawerContent).toBeVisible()
|
||||
await expect(relationToSelector).toBeVisible()
|
||||
await relationToSelector.locator('.rs__control').click()
|
||||
await option.click()
|
||||
const newRows = listDrawerContent.locator('table tbody tr')
|
||||
await expect(newRows).toHaveCount(1)
|
||||
const newFirstRow = newRows.first()
|
||||
const newButton = newFirstRow.locator('button')
|
||||
await newButton.click()
|
||||
await expect(listDrawerContent).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
async function createTextFieldDoc(overrides?: Partial<TextField>): Promise<TextField> {
|
||||
|
||||
@@ -158,7 +158,7 @@ const RelationshipFields: CollectionConfig = {
|
||||
},
|
||||
{
|
||||
name: 'relationshipDrawerHasManyPolymorphic',
|
||||
relationTo: ['text-fields'],
|
||||
relationTo: ['text-fields', 'array-fields'],
|
||||
admin: {
|
||||
appearance: 'drawer',
|
||||
},
|
||||
|
||||
@@ -228,6 +228,12 @@ describe('Form State', () => {
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
title: 'Test Post',
|
||||
blocks: [
|
||||
{
|
||||
blockType: 'text',
|
||||
text: 'Test block',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -248,6 +254,7 @@ describe('Form State', () => {
|
||||
})
|
||||
|
||||
expect(state.title?.addedByServer).toBe(true)
|
||||
expect(state['blocks.0.blockType']?.addedByServer).toBe(true)
|
||||
|
||||
// Ensure that `addedByServer` is removed after being received by the client
|
||||
const newState = mergeServerFormState({
|
||||
|
||||
@@ -6,12 +6,18 @@ import { wait } from 'payload/shared'
|
||||
export async function openDocDrawer({
|
||||
page,
|
||||
selector,
|
||||
withMetaKey = false,
|
||||
}: {
|
||||
page: Page
|
||||
selector: string
|
||||
withMetaKey?: boolean
|
||||
}): Promise<void> {
|
||||
let clickProperties = {}
|
||||
if (withMetaKey) {
|
||||
clickProperties = { modifiers: ['ControlOrMeta'] }
|
||||
}
|
||||
await wait(500) // wait for parent form state to initialize
|
||||
await page.locator(selector).click()
|
||||
await page.locator(selector).click(clickProperties)
|
||||
await wait(500) // wait for drawer form state to initialize
|
||||
}
|
||||
|
||||
|
||||
@@ -940,6 +940,94 @@ describe('Joins Field', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should have simple paginate with page for joins polymorphic', async () => {
|
||||
let queryWithLimit = `query {
|
||||
Categories(where: {
|
||||
name: { equals: "paginate example" }
|
||||
}) {
|
||||
docs {
|
||||
polymorphic(
|
||||
sort: "createdAt",
|
||||
limit: 2
|
||||
) {
|
||||
docs {
|
||||
title
|
||||
}
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
let pageWithLimit = await restClient
|
||||
.GRAPHQL_POST({ body: JSON.stringify({ query: queryWithLimit }) })
|
||||
.then((res) => res.json())
|
||||
|
||||
const queryUnlimited = `query {
|
||||
Categories(
|
||||
where: {
|
||||
name: { equals: "paginate example" }
|
||||
}
|
||||
) {
|
||||
docs {
|
||||
polymorphic(
|
||||
sort: "createdAt",
|
||||
limit: 0
|
||||
) {
|
||||
docs {
|
||||
title
|
||||
createdAt
|
||||
}
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
const unlimited = await restClient
|
||||
.GRAPHQL_POST({ body: JSON.stringify({ query: queryUnlimited }) })
|
||||
.then((res) => res.json())
|
||||
|
||||
expect(pageWithLimit.data.Categories.docs[0].polymorphic.docs).toHaveLength(2)
|
||||
expect(pageWithLimit.data.Categories.docs[0].polymorphic.docs[0].id).toStrictEqual(
|
||||
unlimited.data.Categories.docs[0].polymorphic.docs[0].id,
|
||||
)
|
||||
expect(pageWithLimit.data.Categories.docs[0].polymorphic.docs[1].id).toStrictEqual(
|
||||
unlimited.data.Categories.docs[0].polymorphic.docs[1].id,
|
||||
)
|
||||
|
||||
expect(pageWithLimit.data.Categories.docs[0].polymorphic.hasNextPage).toStrictEqual(true)
|
||||
|
||||
queryWithLimit = `query {
|
||||
Categories(where: {
|
||||
name: { equals: "paginate example" }
|
||||
}) {
|
||||
docs {
|
||||
polymorphic(
|
||||
sort: "createdAt",
|
||||
limit: 2,
|
||||
page: 2,
|
||||
) {
|
||||
docs {
|
||||
title
|
||||
}
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
pageWithLimit = await restClient
|
||||
.GRAPHQL_POST({ body: JSON.stringify({ query: queryWithLimit }) })
|
||||
.then((res) => res.json())
|
||||
|
||||
expect(pageWithLimit.data.Categories.docs[0].polymorphic.docs[0].id).toStrictEqual(
|
||||
unlimited.data.Categories.docs[0].polymorphic.docs[2].id,
|
||||
)
|
||||
expect(pageWithLimit.data.Categories.docs[0].polymorphic.docs[1].id).toStrictEqual(
|
||||
unlimited.data.Categories.docs[0].polymorphic.docs[3].id,
|
||||
)
|
||||
})
|
||||
|
||||
it('should populate joins with hasMany when on both sides documents are in draft', async () => {
|
||||
const category = await payload.create({
|
||||
collection: 'categories-versions',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
import { type Config } from 'payload'
|
||||
|
||||
import { LexicalFullyFeatured } from './collections/_LexicalFullyFeatured/index.js'
|
||||
import ArrayFields from './collections/Array/index.js'
|
||||
import {
|
||||
getLexicalFieldsCollection,
|
||||
@@ -26,6 +27,7 @@ const dirname = path.dirname(filename)
|
||||
export const baseConfig: Partial<Config> = {
|
||||
// ...extend config here
|
||||
collections: [
|
||||
LexicalFullyFeatured,
|
||||
getLexicalFieldsCollection({
|
||||
blocks: lexicalBlocks,
|
||||
inlineBlocks: lexicalInlineBlocks,
|
||||
@@ -42,10 +44,18 @@ export const baseConfig: Partial<Config> = {
|
||||
ArrayFields,
|
||||
],
|
||||
globals: [TabsWithRichText],
|
||||
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
components: {
|
||||
beforeDashboard: [
|
||||
{
|
||||
path: './components/CollectionsExplained.tsx#CollectionsExplained',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
onInit: async (payload) => {
|
||||
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
|
||||
|
||||
@@ -302,7 +302,7 @@ describe('lexicalBlocks', () => {
|
||||
await assertLexicalDoc({
|
||||
fn: ({ lexicalWithBlocks }) => {
|
||||
const rscBlock: SerializedBlockNode = lexicalWithBlocks.root
|
||||
.children[14] as SerializedBlockNode
|
||||
.children[13] as SerializedBlockNode
|
||||
const paragraphNode: SerializedParagraphNode = lexicalWithBlocks.root
|
||||
.children[12] as SerializedParagraphNode
|
||||
|
||||
@@ -1133,9 +1133,9 @@ describe('lexicalBlocks', () => {
|
||||
).docs[0] as never
|
||||
|
||||
const richTextBlock: SerializedBlockNode = lexicalWithBlocks.root
|
||||
.children[13] as SerializedBlockNode
|
||||
.children[12] as SerializedBlockNode
|
||||
const subRichTextBlock: SerializedBlockNode = richTextBlock.fields.richTextField.root
|
||||
.children[1] as SerializedBlockNode // index 0 and 2 are paragraphs created by default around the block node when a new block is added via slash command
|
||||
.children[0] as SerializedBlockNode // index 0 and 2 are paragraphs created by default around the block node when a new block is added via slash command
|
||||
|
||||
const subSubRichTextField = subRichTextBlock.fields.subRichTextField
|
||||
const subSubUploadField = subRichTextBlock.fields.subUploadField
|
||||
@@ -1163,9 +1163,9 @@ describe('lexicalBlocks', () => {
|
||||
).docs[0] as never
|
||||
|
||||
const richTextBlock2: SerializedBlockNode = lexicalWithBlocks.root
|
||||
.children[13] as SerializedBlockNode
|
||||
.children[12] as SerializedBlockNode
|
||||
const subRichTextBlock2: SerializedBlockNode = richTextBlock2.fields.richTextField.root
|
||||
.children[1] as SerializedBlockNode // index 0 and 2 are paragraphs created by default around the block node when a new block is added via slash command
|
||||
.children[0] as SerializedBlockNode // index 0 and 2 are paragraphs created by default around the block node when a new block is added via slash command
|
||||
|
||||
const subSubRichTextField2 = subRichTextBlock2.fields.subRichTextField
|
||||
const subSubUploadField2 = subRichTextBlock2.fields.subUploadField
|
||||
@@ -1666,5 +1666,55 @@ describe('lexicalBlocks', () => {
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('ensure inline blocks restore their state after undoing a removal', async () => {
|
||||
await page.goto('http://localhost:3000/admin/collections/LexicalInBlock?limit=10')
|
||||
|
||||
await page.locator('.cell-id a').first().click()
|
||||
await page.waitForURL(`**/collections/LexicalInBlock/**`)
|
||||
|
||||
// Wait for the page to be fully loaded and elements to be stable
|
||||
await page.waitForLoadState('domcontentloaded')
|
||||
|
||||
// Wait for the specific row to be visible and have its content loaded
|
||||
const row2 = page.locator('#blocks-row-2')
|
||||
await expect(row2).toBeVisible()
|
||||
|
||||
// Get initial count and ensure it's stable
|
||||
const inlineBlocks = page.locator('#blocks-row-2 .inline-block-container')
|
||||
const inlineBlockCount = await inlineBlocks.count()
|
||||
await expect(() => {
|
||||
expect(inlineBlockCount).toBeGreaterThan(0)
|
||||
}).toPass()
|
||||
|
||||
const inlineBlockElement = inlineBlocks.first()
|
||||
await inlineBlockElement.locator('.inline-block__editButton').first().click()
|
||||
|
||||
await page.locator('.drawer--is-open #field-text').fill('value1')
|
||||
await page.locator('.drawer--is-open button[type="submit"]').first().click()
|
||||
|
||||
// remove inline block
|
||||
await inlineBlockElement.click()
|
||||
await page.keyboard.press('Backspace')
|
||||
|
||||
// Check both that this specific element is removed and the total count decreased
|
||||
await expect(inlineBlocks).toHaveCount(inlineBlockCount - 1)
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
await inlineBlockElement.click()
|
||||
|
||||
// Undo the removal using keyboard shortcut
|
||||
await page.keyboard.press('ControlOrMeta+Z')
|
||||
|
||||
// Wait for the block to be restored
|
||||
await expect(inlineBlocks).toHaveCount(inlineBlockCount)
|
||||
|
||||
// Open the drawer again
|
||||
await inlineBlockElement.locator('.inline-block__editButton').first().click()
|
||||
|
||||
// Check if the text field still contains 'value1'
|
||||
await expect(page.locator('.drawer--is-open #field-text')).toHaveValue('value1')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -728,7 +728,8 @@ describe('lexicalMain', () => {
|
||||
await expect(relationshipListDrawer).toBeVisible()
|
||||
await wait(500)
|
||||
|
||||
await expect(relationshipListDrawer.locator('.rs__single-value')).toHaveText('Lexical Field')
|
||||
await relationshipListDrawer.locator('.rs__input').first().click()
|
||||
await relationshipListDrawer.locator('.rs__menu').getByText('Lexical Field').click()
|
||||
|
||||
await relationshipListDrawer.locator('button').getByText('Rich Text').first().click()
|
||||
await expect(relationshipListDrawer).toBeHidden()
|
||||
@@ -1203,10 +1204,11 @@ describe('lexicalMain', () => {
|
||||
await expect(newUploadNode.locator('.lexical-upload__bottomRow')).toContainText('payload.png')
|
||||
|
||||
await page.keyboard.press('Enter') // floating toolbar needs to appear with enough distance to the upload node, otherwise clicking may fail
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.press('ArrowLeft')
|
||||
await page.keyboard.press('ArrowLeft')
|
||||
// Select "there" by pressing shift + arrow left
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await page.keyboard.press('Shift+ArrowLeft')
|
||||
}
|
||||
|
||||
@@ -1258,10 +1260,10 @@ describe('lexicalMain', () => {
|
||||
const firstParagraph: SerializedParagraphNode = lexicalField.root
|
||||
.children[0] as SerializedParagraphNode
|
||||
const secondParagraph: SerializedParagraphNode = lexicalField.root
|
||||
.children[1] as SerializedParagraphNode
|
||||
const thirdParagraph: SerializedParagraphNode = lexicalField.root
|
||||
.children[2] as SerializedParagraphNode
|
||||
const uploadNode: SerializedUploadNode = lexicalField.root.children[3] as SerializedUploadNode
|
||||
const thirdParagraph: SerializedParagraphNode = lexicalField.root
|
||||
.children[3] as SerializedParagraphNode
|
||||
const uploadNode: SerializedUploadNode = lexicalField.root.children[1] as SerializedUploadNode
|
||||
|
||||
expect(firstParagraph.children).toHaveLength(2)
|
||||
expect((firstParagraph.children[0] as SerializedTextNode).text).toBe('Some ')
|
||||
@@ -1391,7 +1393,7 @@ describe('lexicalMain', () => {
|
||||
const lexicalField: SerializedEditorState = lexicalDoc.lexicalRootEditor
|
||||
|
||||
// @ts-expect-error no need to type this
|
||||
expect(lexicalField?.root?.children[1].fields.someTextRequired).toEqual('test')
|
||||
expect(lexicalField?.root?.children[0].fields.someTextRequired).toEqual('test')
|
||||
}).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
|
||||
68
test/lexical/collections/_LexicalFullyFeatured/e2e.spec.ts
Normal file
68
test/lexical/collections/_LexicalFullyFeatured/e2e.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { AdminUrlUtil } from 'helpers/adminUrlUtil.js'
|
||||
import { reInitializeDB } from 'helpers/reInitializeDB.js'
|
||||
import { lexicalFullyFeaturedSlug } from 'lexical/slugs.js'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { ensureCompilationIsDone } from '../../../helpers.js'
|
||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||
import { TEST_TIMEOUT_LONG } from '../../../playwright.config.js'
|
||||
import { LexicalHelpers } from './utils.js'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const currentFolder = path.dirname(filename)
|
||||
const dirname = path.resolve(currentFolder, '../../')
|
||||
|
||||
const { beforeAll, beforeEach, describe } = test
|
||||
|
||||
// Unlike the other suites, this one runs in parallel, as they run on the `lexical-fully-featured/create` URL and are "pure" tests
|
||||
test.describe.configure({ mode: 'parallel' })
|
||||
|
||||
const { serverURL } = await initPayloadE2ENoConfig({
|
||||
dirname,
|
||||
})
|
||||
|
||||
describe('Lexical Fully Featured', () => {
|
||||
beforeAll(async ({ browser }, testInfo) => {
|
||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
|
||||
const page = await browser.newPage()
|
||||
await ensureCompilationIsDone({ page, serverURL })
|
||||
await page.close()
|
||||
})
|
||||
beforeEach(async ({ page }) => {
|
||||
await reInitializeDB({
|
||||
serverURL,
|
||||
snapshotKey: 'fieldsTest',
|
||||
uploadsDir: [
|
||||
path.resolve(dirname, './collections/Upload/uploads'),
|
||||
path.resolve(dirname, './collections/Upload2/uploads2'),
|
||||
],
|
||||
})
|
||||
const url = new AdminUrlUtil(serverURL, lexicalFullyFeaturedSlug)
|
||||
const lexical = new LexicalHelpers(page)
|
||||
await page.goto(url.create)
|
||||
await lexical.editor.first().focus()
|
||||
})
|
||||
test('prevent extra paragraph when inserting decorator blocks like blocks or upload node', async ({
|
||||
page,
|
||||
}) => {
|
||||
const lexical = new LexicalHelpers(page)
|
||||
await lexical.slashCommand('block')
|
||||
await lexical.slashCommand('relationship')
|
||||
await lexical.drawer.locator('.list-drawer__header').getByText('Create New').click()
|
||||
await lexical.save('drawer')
|
||||
await expect(lexical.decorator).toHaveCount(2)
|
||||
await lexical.slashCommand('upload')
|
||||
await lexical.drawer.locator('.list-drawer__header').getByText('Create New').click()
|
||||
await lexical.drawer.getByText('Paste URL').click()
|
||||
await lexical.drawer
|
||||
.locator('.file-field__remote-file')
|
||||
.fill('https://payloadcms.com/images/universal-truth.jpg')
|
||||
await lexical.drawer.getByText('Add file').click()
|
||||
await lexical.save('drawer')
|
||||
await expect(lexical.decorator).toHaveCount(3)
|
||||
const paragraph = lexical.editor.locator('> p')
|
||||
await expect(paragraph).toHaveText('')
|
||||
})
|
||||
})
|
||||
57
test/lexical/collections/_LexicalFullyFeatured/index.ts
Normal file
57
test/lexical/collections/_LexicalFullyFeatured/index.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import {
|
||||
BlocksFeature,
|
||||
EXPERIMENTAL_TableFeature,
|
||||
FixedToolbarFeature,
|
||||
lexicalEditor,
|
||||
TreeViewFeature,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { lexicalFullyFeaturedSlug } from '../../slugs.js'
|
||||
|
||||
export const LexicalFullyFeatured: CollectionConfig = {
|
||||
slug: lexicalFullyFeaturedSlug,
|
||||
labels: {
|
||||
singular: 'Lexical Fully Featured',
|
||||
plural: 'Lexical Fully Featured',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
TreeViewFeature(),
|
||||
FixedToolbarFeature(),
|
||||
EXPERIMENTAL_TableFeature(),
|
||||
BlocksFeature({
|
||||
blocks: [
|
||||
{
|
||||
slug: 'myBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'someText',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
inlineBlocks: [
|
||||
{
|
||||
slug: 'myInlineBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'someText',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
49
test/lexical/collections/_LexicalFullyFeatured/utils.ts
Normal file
49
test/lexical/collections/_LexicalFullyFeatured/utils.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { Page } from 'playwright'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
export class LexicalHelpers {
|
||||
page: Page
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
}
|
||||
|
||||
async save(container: 'document' | 'drawer') {
|
||||
if (container === 'drawer') {
|
||||
await this.drawer.getByText('Save').click()
|
||||
} else {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
await this.page.waitForTimeout(1000)
|
||||
}
|
||||
|
||||
async slashCommand(
|
||||
// prettier-ignore
|
||||
command: 'block' | 'check' | 'code' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' |'h6' | 'inline'
|
||||
| 'link' | 'ordered' | 'paragraph' | 'quote' | 'relationship' | 'unordered' | 'upload',
|
||||
) {
|
||||
await this.page.keyboard.press(`/`)
|
||||
|
||||
const slashMenuPopover = this.page.locator('#slash-menu .slash-menu-popup')
|
||||
await expect(slashMenuPopover).toBeVisible()
|
||||
await this.page.keyboard.type(command)
|
||||
await this.page.keyboard.press(`Enter`)
|
||||
await expect(slashMenuPopover).toBeHidden()
|
||||
}
|
||||
|
||||
get decorator() {
|
||||
return this.editor.locator('[data-lexical-decorator="true"]')
|
||||
}
|
||||
|
||||
get drawer() {
|
||||
return this.page.locator('.drawer__content')
|
||||
}
|
||||
|
||||
get editor() {
|
||||
return this.page.locator('[data-lexical-editor="true"]')
|
||||
}
|
||||
|
||||
get paragraph() {
|
||||
return this.editor.locator('p')
|
||||
}
|
||||
}
|
||||
22
test/lexical/components/CollectionsExplained.tsx
Normal file
22
test/lexical/components/CollectionsExplained.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
|
||||
export function CollectionsExplained() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Which collection should I use for my tests?</h1>
|
||||
|
||||
<p>
|
||||
By default and as a rule of thumb: "Lexical Fully Featured". This collection has all our
|
||||
features, but it does NOT have (and will never have):
|
||||
</p>
|
||||
<ul>
|
||||
<li>Relationships or dependencies to other collections</li>
|
||||
<li>Seeded documents</li>
|
||||
<li>Features with custom props (except for a block and an inline block included)</li>
|
||||
<li>Multiple richtext fields or other fields</li>
|
||||
</ul>
|
||||
|
||||
<p>If you need any of these features, use another collection or create a new one.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -83,6 +83,7 @@ export interface Config {
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
'lexical-fully-featured': LexicalFullyFeatured;
|
||||
'lexical-fields': LexicalField;
|
||||
'lexical-migrate-fields': LexicalMigrateField;
|
||||
'lexical-localized-fields': LexicalLocalizedField;
|
||||
@@ -101,6 +102,7 @@ export interface Config {
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
'lexical-fully-featured': LexicalFullyFeaturedSelect<false> | LexicalFullyFeaturedSelect<true>;
|
||||
'lexical-fields': LexicalFieldsSelect<false> | LexicalFieldsSelect<true>;
|
||||
'lexical-migrate-fields': LexicalMigrateFieldsSelect<false> | LexicalMigrateFieldsSelect<true>;
|
||||
'lexical-localized-fields': LexicalLocalizedFieldsSelect<false> | LexicalLocalizedFieldsSelect<true>;
|
||||
@@ -153,6 +155,30 @@ export interface UserAuthOperations {
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "lexical-fully-featured".
|
||||
*/
|
||||
export interface LexicalFullyFeatured {
|
||||
id: string;
|
||||
richText?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "lexical-fields".
|
||||
@@ -774,6 +800,10 @@ export interface User {
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'lexical-fully-featured';
|
||||
value: string | LexicalFullyFeatured;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'lexical-fields';
|
||||
value: string | LexicalField;
|
||||
@@ -864,6 +894,15 @@ export interface PayloadMigration {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "lexical-fully-featured_select".
|
||||
*/
|
||||
export interface LexicalFullyFeaturedSelect<T extends boolean = true> {
|
||||
richText?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "lexical-fields_select".
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const usersSlug = 'users'
|
||||
|
||||
export const lexicalFullyFeaturedSlug = 'lexical-fully-featured'
|
||||
export const lexicalFieldsSlug = 'lexical-fields'
|
||||
export const lexicalLocalizedFieldsSlug = 'lexical-localized-fields'
|
||||
export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields'
|
||||
|
||||
@@ -13,9 +13,9 @@ import { fileURLToPath } from 'url'
|
||||
|
||||
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||
import type { Media, Page, Post, Tenant } from './payload-types.js'
|
||||
import config from './config.js'
|
||||
|
||||
import { Pages } from './collections/Pages.js'
|
||||
import config from './config.js'
|
||||
import { postsSlug, tenantsSlug } from './shared.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
@@ -28,7 +28,7 @@ let restClient: NextRESTClient
|
||||
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
|
||||
function collectionPopulationRequestHandler({ endpoint }: { endpoint: string }) {
|
||||
function requestHandler({ endpoint }: { endpoint: string }) {
|
||||
return restClient.GET(`/${endpoint}`)
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(mergedData.title).toEqual('Test Page (Changed)')
|
||||
@@ -198,7 +198,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(mergedData.arrayOfRelationships).toEqual([])
|
||||
@@ -217,7 +217,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(mergedData2.arrayOfRelationships).toEqual([])
|
||||
@@ -243,7 +243,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(mergedData.hero.media).toMatchObject(media)
|
||||
@@ -262,7 +262,7 @@ describe('Collections - Live Preview', () => {
|
||||
},
|
||||
initialData: mergedData,
|
||||
serverURL,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(mergedDataWithoutUpload.hero.media).toBeFalsy()
|
||||
@@ -290,7 +290,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge1.richTextSlate).toHaveLength(1)
|
||||
@@ -317,7 +317,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge2.richTextSlate).toHaveLength(1)
|
||||
@@ -377,7 +377,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge1.richTextLexical.root.children).toHaveLength(2)
|
||||
@@ -423,7 +423,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge2.richTextLexical.root.children).toHaveLength(1)
|
||||
@@ -446,7 +446,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge1._numberOfRequests).toEqual(1)
|
||||
@@ -468,7 +468,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge1._numberOfRequests).toEqual(1)
|
||||
@@ -490,7 +490,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge1._numberOfRequests).toEqual(1)
|
||||
@@ -515,7 +515,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge1._numberOfRequests).toEqual(1)
|
||||
@@ -545,7 +545,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge2._numberOfRequests).toEqual(0)
|
||||
@@ -571,7 +571,7 @@ describe('Collections - Live Preview', () => {
|
||||
},
|
||||
initialData,
|
||||
serverURL,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge1.tab.relationshipInTab).toMatchObject(testPost)
|
||||
@@ -608,7 +608,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge1._numberOfRequests).toEqual(1)
|
||||
@@ -665,7 +665,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData: merge1,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge2._numberOfRequests).toEqual(1)
|
||||
@@ -741,7 +741,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge1._numberOfRequests).toEqual(2)
|
||||
@@ -804,7 +804,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData: merge1,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge2._numberOfRequests).toEqual(1)
|
||||
@@ -870,7 +870,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge1._numberOfRequests).toEqual(2)
|
||||
@@ -937,7 +937,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData: merge1,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge2._numberOfRequests).toEqual(1)
|
||||
@@ -991,7 +991,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge1._numberOfRequests).toEqual(0)
|
||||
@@ -1051,7 +1051,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
// Check that the relationship on the first has been removed
|
||||
@@ -1080,7 +1080,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge1._numberOfRequests).toEqual(1)
|
||||
@@ -1126,7 +1126,7 @@ describe('Collections - Live Preview', () => {
|
||||
externallyUpdatedRelationship,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
expect(merge2._numberOfRequests).toEqual(1)
|
||||
@@ -1183,7 +1183,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
locale: 'es',
|
||||
})
|
||||
|
||||
@@ -1332,7 +1332,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
// Check that the blocks have been reordered
|
||||
@@ -1365,7 +1365,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
// Check that the block has been removed
|
||||
@@ -1385,7 +1385,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler,
|
||||
requestHandler,
|
||||
})
|
||||
|
||||
// Check that the block has been removed
|
||||
@@ -1445,7 +1445,7 @@ describe('Collections - Live Preview', () => {
|
||||
initialData,
|
||||
serverURL,
|
||||
returnNumberOfRequests: true,
|
||||
collectionPopulationRequestHandler: customRequestHandler,
|
||||
requestHandler: customRequestHandler,
|
||||
})
|
||||
|
||||
expect(mergedData.relationshipPolyHasMany).toMatchObject([
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
import { blocksCollectionSlug } from './collections/Blocks/index.js'
|
||||
import { nestedToArrayAndBlockCollectionSlug } from './collections/NestedToArrayAndBlock/index.js'
|
||||
import { richTextSlug } from './collections/RichText/index.js'
|
||||
import {
|
||||
@@ -427,6 +428,30 @@ describe('Localization', () => {
|
||||
await expect(arrayField).toHaveValue(sampleText)
|
||||
})
|
||||
|
||||
test('should copy block to locale', async () => {
|
||||
const sampleText = 'Copy this text'
|
||||
const blocksCollection = new AdminUrlUtil(serverURL, blocksCollectionSlug)
|
||||
await page.goto(blocksCollection.create)
|
||||
await changeLocale(page, 'pt')
|
||||
const addBlock = page.locator('.blocks-field__drawer-toggler')
|
||||
await addBlock.click()
|
||||
const selectBlock = page.locator('.blocks-drawer__block button')
|
||||
await selectBlock.click()
|
||||
const addContentButton = page.locator('#field-content__0__content button')
|
||||
await addContentButton.click()
|
||||
await selectBlock.click()
|
||||
const textField = page.locator('#field-content__0__content__0__text')
|
||||
await expect(textField).toBeVisible()
|
||||
await textField.fill(sampleText)
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
await openCopyToLocaleDrawer(page)
|
||||
await setToLocale(page, 'English')
|
||||
await runCopy(page)
|
||||
|
||||
await expect(textField).toHaveValue(sampleText)
|
||||
})
|
||||
|
||||
test('should default source locale to current locale', async () => {
|
||||
await changeLocale(page, spanishLocale)
|
||||
await createAndSaveDoc(page, url, { title })
|
||||
|
||||
@@ -98,6 +98,19 @@ export const Pages: CollectionConfig = {
|
||||
type: 'relationship',
|
||||
relationTo: 'users',
|
||||
},
|
||||
{
|
||||
name: 'virtualRelationship',
|
||||
type: 'text',
|
||||
virtual: 'author.name',
|
||||
},
|
||||
{
|
||||
name: 'virtual',
|
||||
type: 'text',
|
||||
virtual: true,
|
||||
hooks: {
|
||||
afterRead: [() => 'virtual value'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'hasManyNumber',
|
||||
type: 'number',
|
||||
|
||||
@@ -10,6 +10,10 @@ export const Users: CollectionConfig = {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
// Email added by default
|
||||
// Add more fields as needed
|
||||
],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CollectionSlug, Payload } from 'payload'
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -221,6 +222,68 @@ describe('@payloadcms/plugin-import-export', () => {
|
||||
expect(data[0].array_1_field2).toStrictEqual('baz')
|
||||
})
|
||||
|
||||
it('should create a CSV file with columns matching the order of the fields array', async () => {
|
||||
const fields = ['id', 'group.value', 'group.array.field1', 'title', 'createdAt', 'updatedAt']
|
||||
const doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collectionSlug: 'pages',
|
||||
fields,
|
||||
format: 'csv',
|
||||
where: {
|
||||
title: { contains: 'Title ' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const exportDoc = await payload.findByID({
|
||||
collection: 'exports',
|
||||
id: doc.id,
|
||||
})
|
||||
|
||||
expect(exportDoc.filename).toBeDefined()
|
||||
const expectedPath = path.join(dirname, './uploads', exportDoc.filename as string)
|
||||
const buffer = fs.readFileSync(expectedPath)
|
||||
const str = buffer.toString()
|
||||
|
||||
// Assert that the header row matches the fields array
|
||||
expect(str.indexOf('id')).toBeLessThan(str.indexOf('title'))
|
||||
expect(str.indexOf('group_value')).toBeLessThan(str.indexOf('title'))
|
||||
expect(str.indexOf('group_value')).toBeLessThan(str.indexOf('group_array'))
|
||||
expect(str.indexOf('title')).toBeLessThan(str.indexOf('createdAt'))
|
||||
expect(str.indexOf('createdAt')).toBeLessThan(str.indexOf('updatedAt'))
|
||||
})
|
||||
|
||||
it('should create a CSV file with virtual fields', async () => {
|
||||
const fields = ['id', 'virtual', 'virtualRelationship']
|
||||
const doc = await payload.create({
|
||||
collection: 'exports',
|
||||
user,
|
||||
data: {
|
||||
collectionSlug: 'pages',
|
||||
fields,
|
||||
format: 'csv',
|
||||
where: {
|
||||
title: { contains: 'Virtual ' },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const exportDoc = await payload.findByID({
|
||||
collection: 'exports',
|
||||
id: doc.id,
|
||||
})
|
||||
|
||||
expect(exportDoc.filename).toBeDefined()
|
||||
const expectedPath = path.join(dirname, './uploads', exportDoc.filename as string)
|
||||
const data = await readCSV(expectedPath)
|
||||
|
||||
// Assert that the csv file contains the expected virtual fields
|
||||
expect(data[0].virtual).toStrictEqual('virtual value')
|
||||
expect(data[0].virtualRelationship).toStrictEqual('name value')
|
||||
})
|
||||
|
||||
it('should create a file for collection csv from array.subfield', async () => {
|
||||
let doc = await payload.create({
|
||||
collection: 'exports',
|
||||
|
||||
@@ -131,6 +131,7 @@ export interface UserAuthOperations {
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@@ -199,6 +200,8 @@ export interface Page {
|
||||
)[]
|
||||
| null;
|
||||
author?: (string | null) | User;
|
||||
virtualRelationship?: string | null;
|
||||
virtual?: string | null;
|
||||
hasManyNumber?: number[] | null;
|
||||
relationship?: (string | null) | User;
|
||||
excerpt?: string | null;
|
||||
@@ -444,6 +447,7 @@ export interface PayloadMigration {
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
@@ -500,6 +504,8 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
};
|
||||
};
|
||||
author?: T;
|
||||
virtualRelationship?: T;
|
||||
virtual?: T;
|
||||
hasManyNumber?: T;
|
||||
relationship?: T;
|
||||
excerpt?: T;
|
||||
|
||||
@@ -6,11 +6,12 @@ import { richTextData } from './richTextData.js'
|
||||
export const seed = async (payload: Payload): Promise<boolean> => {
|
||||
payload.logger.info('Seeding data...')
|
||||
try {
|
||||
await payload.create({
|
||||
const user = await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
name: 'name value',
|
||||
},
|
||||
})
|
||||
// create pages
|
||||
@@ -80,6 +81,16 @@ export const seed = async (payload: Payload): Promise<boolean> => {
|
||||
})
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
author: user.id,
|
||||
title: `Virtual ${i}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
|
||||
@@ -444,6 +444,43 @@ describe('Sort', () => {
|
||||
parseInt(ordered.docs[1]._order, 16),
|
||||
)
|
||||
})
|
||||
|
||||
it('should allow to duplicate with reordable', async () => {
|
||||
const doc = await payload.create({
|
||||
collection: 'orderable',
|
||||
data: { title: 'new document' },
|
||||
})
|
||||
|
||||
const docDuplicated = await payload.create({
|
||||
duplicateFromID: doc.id,
|
||||
collection: 'orderable',
|
||||
data: {},
|
||||
})
|
||||
expect(docDuplicated.title).toBe('new document')
|
||||
expect(parseInt(doc._order!, 16)).toBeLessThan(parseInt(docDuplicated._order!, 16))
|
||||
|
||||
await restClient.POST('/reorder', {
|
||||
body: JSON.stringify({
|
||||
collectionSlug: orderableSlug,
|
||||
docsToMove: [doc.id],
|
||||
newKeyWillBe: 'greater',
|
||||
orderableFieldName: '_order',
|
||||
target: {
|
||||
id: docDuplicated.id,
|
||||
key: docDuplicated._order,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const docAfterReorder = await payload.findByID({ collection: 'orderable', id: doc.id })
|
||||
const docDuplicatedAfterReorder = await payload.findByID({
|
||||
collection: 'orderable',
|
||||
id: docDuplicated.id,
|
||||
})
|
||||
expect(parseInt(docAfterReorder._order!, 16)).toBeGreaterThan(
|
||||
parseInt(docDuplicatedAfterReorder._order!, 16),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Orderable join', () => {
|
||||
|
||||
@@ -205,6 +205,116 @@ describe('Versions', () => {
|
||||
await expect(page.locator('#field-title')).toHaveValue('v1')
|
||||
})
|
||||
|
||||
test('should show currently published version status in versions view', async () => {
|
||||
const publishedDoc = await payload.create({
|
||||
collection: draftCollectionSlug,
|
||||
data: {
|
||||
_status: 'published',
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
},
|
||||
overrideAccess: true,
|
||||
})
|
||||
|
||||
await page.goto(`${url.edit(publishedDoc.id)}/versions`)
|
||||
await expect(page.locator('main.versions')).toContainText('Current Published Version')
|
||||
})
|
||||
|
||||
test('should show unpublished version status in versions view', async () => {
|
||||
const publishedDoc = await payload.create({
|
||||
collection: draftCollectionSlug,
|
||||
data: {
|
||||
_status: 'published',
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
},
|
||||
overrideAccess: true,
|
||||
})
|
||||
|
||||
// Unpublish the document
|
||||
await payload.update({
|
||||
collection: draftCollectionSlug,
|
||||
id: publishedDoc.id,
|
||||
data: {
|
||||
_status: 'draft',
|
||||
},
|
||||
draft: false,
|
||||
})
|
||||
|
||||
await page.goto(`${url.edit(publishedDoc.id)}/versions`)
|
||||
await expect(page.locator('main.versions')).toContainText('Previously Published')
|
||||
})
|
||||
|
||||
test('should show global versions view level action in globals versions view', async () => {
|
||||
const global = new AdminUrlUtil(serverURL, draftGlobalSlug)
|
||||
await page.goto(`${global.global(draftGlobalSlug)}/versions`)
|
||||
await expect(page.locator('.app-header .global-versions-button')).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('global — has versions tab', async () => {
|
||||
const global = new AdminUrlUtil(serverURL, draftGlobalSlug)
|
||||
await page.goto(global.global(draftGlobalSlug))
|
||||
|
||||
const docURL = page.url()
|
||||
const pathname = new URL(docURL).pathname
|
||||
|
||||
const versionsTab = page.locator('.doc-tab', {
|
||||
hasText: 'Versions',
|
||||
})
|
||||
await versionsTab.waitFor({ state: 'visible' })
|
||||
|
||||
expect(versionsTab).toBeTruthy()
|
||||
const href = versionsTab.locator('a').first()
|
||||
await expect(href).toHaveAttribute('href', `${pathname}/versions`)
|
||||
})
|
||||
|
||||
test('global — respects max number of versions', async () => {
|
||||
await payload.updateGlobal({
|
||||
slug: draftWithMaxGlobalSlug,
|
||||
data: {
|
||||
title: 'initial title',
|
||||
},
|
||||
})
|
||||
|
||||
const global = new AdminUrlUtil(serverURL, draftWithMaxGlobalSlug)
|
||||
await page.goto(global.global(draftWithMaxGlobalSlug))
|
||||
|
||||
const titleFieldInitial = page.locator('#field-title')
|
||||
await titleFieldInitial.fill('updated title')
|
||||
await saveDocAndAssert(page, '#action-save-draft')
|
||||
await expect(titleFieldInitial).toHaveValue('updated title')
|
||||
|
||||
const versionsTab = page.locator('.doc-tab', {
|
||||
hasText: '1',
|
||||
})
|
||||
|
||||
await versionsTab.waitFor({ state: 'visible' })
|
||||
|
||||
expect(versionsTab).toBeTruthy()
|
||||
|
||||
const titleFieldUpdated = page.locator('#field-title')
|
||||
await titleFieldUpdated.fill('latest title')
|
||||
await saveDocAndAssert(page, '#action-save-draft')
|
||||
await expect(titleFieldUpdated).toHaveValue('latest title')
|
||||
|
||||
const versionsTabUpdated = page.locator('.doc-tab', {
|
||||
hasText: '1',
|
||||
})
|
||||
|
||||
await versionsTabUpdated.waitFor({ state: 'visible' })
|
||||
|
||||
expect(versionsTabUpdated).toBeTruthy()
|
||||
})
|
||||
|
||||
test('global — has versions route', async () => {
|
||||
const global = new AdminUrlUtil(serverURL, autoSaveGlobalSlug)
|
||||
const versionsURL = `${global.global(autoSaveGlobalSlug)}/versions`
|
||||
await page.goto(versionsURL)
|
||||
await expect(() => {
|
||||
expect(page.url()).toMatch(/\/versions/)
|
||||
}).toPass({ timeout: 10000, intervals: [100] })
|
||||
})
|
||||
|
||||
test('collection - should autosave', async () => {
|
||||
await page.goto(autosaveURL.create)
|
||||
await page.locator('#field-title').fill('autosave title')
|
||||
|
||||
Reference in New Issue
Block a user