Merge branch 'main' into feat/folders
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -54,6 +54,7 @@ export type SupportedTimezones =
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
|
||||
@@ -205,6 +205,37 @@ export const Posts: CollectionConfig = {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'radio',
|
||||
name: 'wavelengths',
|
||||
defaultValue: 'fm',
|
||||
options: [
|
||||
{
|
||||
label: 'FM',
|
||||
value: 'fm',
|
||||
},
|
||||
{
|
||||
label: 'AM',
|
||||
value: 'am',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'selectField',
|
||||
hasMany: true,
|
||||
defaultValue: ['option1', 'option2'],
|
||||
options: [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: 'option1',
|
||||
},
|
||||
{
|
||||
label: 'Option 2',
|
||||
value: 'option2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
labels: {
|
||||
plural: slugPluralLabel,
|
||||
|
||||
@@ -392,7 +392,7 @@ describe('List View', () => {
|
||||
test('should reset filter value when a different field is selected', async () => {
|
||||
const id = (await page.locator('.cell-id').first().innerText()).replace('ID: ', '')
|
||||
|
||||
const whereBuilder = await addListFilter({
|
||||
const { whereBuilder } = await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'ID',
|
||||
operatorLabel: 'equals',
|
||||
@@ -413,10 +413,11 @@ describe('List View', () => {
|
||||
await expect(whereBuilder.locator('.condition__value input')).toHaveValue('')
|
||||
})
|
||||
|
||||
|
||||
test('should remove condition from URL when value is cleared', async () => {
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
const whereBuilder = await addListFilter({
|
||||
const { whereBuilder } = await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Relationship',
|
||||
operatorLabel: 'equals',
|
||||
@@ -431,16 +432,13 @@ describe('List View', () => {
|
||||
await whereBuilder.locator('.condition__value .clear-indicator').click()
|
||||
|
||||
await page.waitForURL(new RegExp(encodedQueryString))
|
||||
})
|
||||
|
||||
test.skip('should remove condition from URL when a different field is selected', async () => {
|
||||
// TODO: fix this bug and write this test
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
|
||||
test('should refresh relationship values when a different field is selected', async () => {
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
const whereBuilder = await addListFilter({
|
||||
const { whereBuilder } = await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Relationship',
|
||||
operatorLabel: 'equals',
|
||||
@@ -600,7 +598,7 @@ describe('List View', () => {
|
||||
test('should reset filter values for every additional filter', async () => {
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
const whereBuilder = await addListFilter({
|
||||
const { whereBuilder } = await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Tab 1 > Title',
|
||||
operatorLabel: 'equals',
|
||||
@@ -622,7 +620,7 @@ describe('List View', () => {
|
||||
test('should not re-render page upon typing in a value in the filter value field', async () => {
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
const whereBuilder = await addListFilter({
|
||||
const { whereBuilder } = await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Tab 1 > Title',
|
||||
operatorLabel: 'equals',
|
||||
@@ -645,7 +643,7 @@ describe('List View', () => {
|
||||
test('should still show second filter if two filters exist and first filter is removed', async () => {
|
||||
await page.goto(postsUrl.list)
|
||||
|
||||
const whereBuilder = await addListFilter({
|
||||
const { whereBuilder } = await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Tab 1 > Title',
|
||||
operatorLabel: 'equals',
|
||||
@@ -739,7 +737,7 @@ describe('List View', () => {
|
||||
test('should properly paginate many documents', async () => {
|
||||
await page.goto(with300DocumentsUrl.list)
|
||||
|
||||
const whereBuilder = await addListFilter({
|
||||
const { whereBuilder } = await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Self Relation',
|
||||
operatorLabel: 'equals',
|
||||
@@ -1288,6 +1286,31 @@ describe('List View', () => {
|
||||
await expect(page.locator('#heading-_status')).toBeVisible()
|
||||
await expect(page.locator('.cell-_status').first()).toBeVisible()
|
||||
|
||||
await toggleColumn(page, {
|
||||
columnLabel: 'Wavelengths',
|
||||
targetState: 'on',
|
||||
columnName: 'wavelengths',
|
||||
})
|
||||
|
||||
await toggleColumn(page, {
|
||||
columnLabel: 'Select Field',
|
||||
targetState: 'on',
|
||||
columnName: 'selectField',
|
||||
})
|
||||
|
||||
// check that the cells have the classes added per value selected
|
||||
await expect(
|
||||
page.locator('.cell-_status').first().locator("[class*='selected--']"),
|
||||
).toBeVisible()
|
||||
|
||||
await expect(
|
||||
page.locator('.cell-wavelengths').first().locator("[class*='selected--']"),
|
||||
).toBeVisible()
|
||||
|
||||
await expect(
|
||||
page.locator('.cell-selectField').first().locator("[class*='selected--']"),
|
||||
).toBeVisible()
|
||||
|
||||
// sort by title again in descending order
|
||||
await page.locator('#heading-title button.sort-column__desc').click()
|
||||
await page.waitForURL(/sort=-title/)
|
||||
|
||||
@@ -203,6 +203,17 @@ export default buildConfigWithDefaults({
|
||||
// lock_until
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'disable-local-strategy-password',
|
||||
auth: { disableLocalStrategy: true },
|
||||
fields: [
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: apiKeysSlug,
|
||||
access: {
|
||||
|
||||
@@ -786,6 +786,20 @@ describe('Auth', () => {
|
||||
|
||||
expect(response.status).toBe(403)
|
||||
})
|
||||
|
||||
it('should allow to use password field', async () => {
|
||||
const doc = await payload.create({
|
||||
collection: 'disable-local-strategy-password',
|
||||
data: { password: '123' },
|
||||
})
|
||||
expect(doc.password).toBe('123')
|
||||
const updated = await payload.update({
|
||||
collection: 'disable-local-strategy-password',
|
||||
data: { password: '1234' },
|
||||
id: doc.id,
|
||||
})
|
||||
expect(updated.password).toBe('1234')
|
||||
})
|
||||
})
|
||||
|
||||
describe('API Key', () => {
|
||||
|
||||
@@ -54,6 +54,7 @@ export type SupportedTimezones =
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
@@ -64,6 +65,7 @@ export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
'partial-disable-local-strategies': PartialDisableLocalStrategyAuthOperations;
|
||||
'disable-local-strategy-password': DisableLocalStrategyPasswordAuthOperations;
|
||||
'api-keys': ApiKeyAuthOperations;
|
||||
'public-users': PublicUserAuthOperations;
|
||||
};
|
||||
@@ -71,6 +73,7 @@ export interface Config {
|
||||
collections: {
|
||||
users: User;
|
||||
'partial-disable-local-strategies': PartialDisableLocalStrategy;
|
||||
'disable-local-strategy-password': DisableLocalStrategyPassword;
|
||||
'api-keys': ApiKey;
|
||||
'public-users': PublicUser;
|
||||
relationsCollection: RelationsCollection;
|
||||
@@ -82,6 +85,7 @@ export interface Config {
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'partial-disable-local-strategies': PartialDisableLocalStrategiesSelect<false> | PartialDisableLocalStrategiesSelect<true>;
|
||||
'disable-local-strategy-password': DisableLocalStrategyPasswordSelect<false> | DisableLocalStrategyPasswordSelect<true>;
|
||||
'api-keys': ApiKeysSelect<false> | ApiKeysSelect<true>;
|
||||
'public-users': PublicUsersSelect<false> | PublicUsersSelect<true>;
|
||||
relationsCollection: RelationsCollectionSelect<false> | RelationsCollectionSelect<true>;
|
||||
@@ -102,6 +106,9 @@ export interface Config {
|
||||
| (PartialDisableLocalStrategy & {
|
||||
collection: 'partial-disable-local-strategies';
|
||||
})
|
||||
| (DisableLocalStrategyPassword & {
|
||||
collection: 'disable-local-strategy-password';
|
||||
})
|
||||
| (ApiKey & {
|
||||
collection: 'api-keys';
|
||||
})
|
||||
@@ -149,6 +156,24 @@ export interface PartialDisableLocalStrategyAuthOperations {
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
export interface DisableLocalStrategyPasswordAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
export interface ApiKeyAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
@@ -242,6 +267,16 @@ export interface PartialDisableLocalStrategy {
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "disable-local-strategy-password".
|
||||
*/
|
||||
export interface DisableLocalStrategyPassword {
|
||||
id: string;
|
||||
password: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "api-keys".
|
||||
@@ -299,6 +334,10 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'partial-disable-local-strategies';
|
||||
value: string | PartialDisableLocalStrategy;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'disable-local-strategy-password';
|
||||
value: string | DisableLocalStrategyPassword;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'api-keys';
|
||||
value: string | ApiKey;
|
||||
@@ -321,6 +360,10 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'partial-disable-local-strategies';
|
||||
value: string | PartialDisableLocalStrategy;
|
||||
}
|
||||
| {
|
||||
relationTo: 'disable-local-strategy-password';
|
||||
value: string | DisableLocalStrategyPassword;
|
||||
}
|
||||
| {
|
||||
relationTo: 'api-keys';
|
||||
value: string | ApiKey;
|
||||
@@ -347,6 +390,10 @@ export interface PayloadPreference {
|
||||
relationTo: 'partial-disable-local-strategies';
|
||||
value: string | PartialDisableLocalStrategy;
|
||||
}
|
||||
| {
|
||||
relationTo: 'disable-local-strategy-password';
|
||||
value: string | DisableLocalStrategyPassword;
|
||||
}
|
||||
| {
|
||||
relationTo: 'api-keys';
|
||||
value: string | ApiKey;
|
||||
@@ -440,6 +487,15 @@ export interface PartialDisableLocalStrategiesSelect<T extends boolean = true> {
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "disable-local-strategy-password_select".
|
||||
*/
|
||||
export interface DisableLocalStrategyPasswordSelect<T extends boolean = true> {
|
||||
password?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "api-keys_select".
|
||||
|
||||
@@ -2134,6 +2134,16 @@ describe('database', () => {
|
||||
expect(query3.totalDocs).toEqual(1)
|
||||
})
|
||||
|
||||
it('db.deleteOne should not fail if query does not resolve to any document', async () => {
|
||||
await expect(
|
||||
payload.db.deleteOne({
|
||||
collection: 'posts',
|
||||
returning: false,
|
||||
where: { title: { equals: 'some random title' } },
|
||||
}),
|
||||
).resolves.toBeNull()
|
||||
})
|
||||
|
||||
it('mongodb additional keys stripping', async () => {
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
if (payload.db.name !== 'mongoose') {
|
||||
|
||||
58
test/database/up-down-migration/int.spec.ts
Normal file
58
test/database/up-down-migration/int.spec.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
import { existsSync, rmdirSync, rmSync } from 'fs'
|
||||
import path from 'path'
|
||||
import { buildConfig, getPayload } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const describe =
|
||||
process.env.PAYLOAD_DATABASE === 'postgres' ? global.describe : global.describe.skip
|
||||
|
||||
const clearMigrations = () => {
|
||||
if (existsSync(path.resolve(dirname, 'migrations'))) {
|
||||
rmSync(path.resolve(dirname, 'migrations'), { force: true, recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
describe('SQL migrations', () => {
|
||||
// If something fails - an error will be thrown.
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('should up and down migration successfully', async () => {
|
||||
clearMigrations()
|
||||
|
||||
const { databaseAdapter } = await import(path.resolve(dirname, '../../databaseAdapter.js'))
|
||||
|
||||
const init = databaseAdapter.init
|
||||
|
||||
// set options
|
||||
databaseAdapter.init = ({ payload }) => {
|
||||
const adapter = init({ payload })
|
||||
adapter.migrationDir = path.resolve(dirname, 'migrations')
|
||||
adapter.push = false
|
||||
return adapter
|
||||
}
|
||||
|
||||
const config = await buildConfig({
|
||||
db: databaseAdapter,
|
||||
secret: 'secret',
|
||||
collections: [
|
||||
{
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
fields: [],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
await payload.db.createMigration({ payload })
|
||||
await payload.db.migrate()
|
||||
await payload.db.migrateDown()
|
||||
|
||||
await payload.db.dropDatabase({ adapter: payload.db as any })
|
||||
await payload.db.destroy?.()
|
||||
})
|
||||
})
|
||||
463
test/database/up-down-migration/migrations/20250328_185055.json
Normal file
463
test/database/up-down-migration/migrations/20250328_185055.json
Normal file
@@ -0,0 +1,463 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"tables": {
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reset_password_token": {
|
||||
"name": "reset_password_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reset_password_expiration": {
|
||||
"name": "reset_password_expiration",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"salt": {
|
||||
"name": "salt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"hash": {
|
||||
"name": "hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"login_attempts": {
|
||||
"name": "login_attempts",
|
||||
"type": "numeric",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"lock_until": {
|
||||
"name": "lock_until",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_updated_at_idx": {
|
||||
"name": "users_updated_at_idx",
|
||||
"columns": ["updated_at"],
|
||||
"isUnique": false
|
||||
},
|
||||
"users_created_at_idx": {
|
||||
"name": "users_created_at_idx",
|
||||
"columns": ["created_at"],
|
||||
"isUnique": false
|
||||
},
|
||||
"users_email_idx": {
|
||||
"name": "users_email_idx",
|
||||
"columns": ["email"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"payload_locked_documents": {
|
||||
"name": "payload_locked_documents",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"global_slug": {
|
||||
"name": "global_slug",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"payload_locked_documents_global_slug_idx": {
|
||||
"name": "payload_locked_documents_global_slug_idx",
|
||||
"columns": ["global_slug"],
|
||||
"isUnique": false
|
||||
},
|
||||
"payload_locked_documents_updated_at_idx": {
|
||||
"name": "payload_locked_documents_updated_at_idx",
|
||||
"columns": ["updated_at"],
|
||||
"isUnique": false
|
||||
},
|
||||
"payload_locked_documents_created_at_idx": {
|
||||
"name": "payload_locked_documents_created_at_idx",
|
||||
"columns": ["created_at"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"payload_locked_documents_rels": {
|
||||
"name": "payload_locked_documents_rels",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"order": {
|
||||
"name": "order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"parent_id": {
|
||||
"name": "parent_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"users_id": {
|
||||
"name": "users_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"payload_locked_documents_rels_order_idx": {
|
||||
"name": "payload_locked_documents_rels_order_idx",
|
||||
"columns": ["order"],
|
||||
"isUnique": false
|
||||
},
|
||||
"payload_locked_documents_rels_parent_idx": {
|
||||
"name": "payload_locked_documents_rels_parent_idx",
|
||||
"columns": ["parent_id"],
|
||||
"isUnique": false
|
||||
},
|
||||
"payload_locked_documents_rels_path_idx": {
|
||||
"name": "payload_locked_documents_rels_path_idx",
|
||||
"columns": ["path"],
|
||||
"isUnique": false
|
||||
},
|
||||
"payload_locked_documents_rels_users_id_idx": {
|
||||
"name": "payload_locked_documents_rels_users_id_idx",
|
||||
"columns": ["users_id"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"payload_locked_documents_rels_parent_fk": {
|
||||
"name": "payload_locked_documents_rels_parent_fk",
|
||||
"tableFrom": "payload_locked_documents_rels",
|
||||
"tableTo": "payload_locked_documents",
|
||||
"columnsFrom": ["parent_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"payload_locked_documents_rels_users_fk": {
|
||||
"name": "payload_locked_documents_rels_users_fk",
|
||||
"tableFrom": "payload_locked_documents_rels",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["users_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"payload_preferences": {
|
||||
"name": "payload_preferences",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"payload_preferences_key_idx": {
|
||||
"name": "payload_preferences_key_idx",
|
||||
"columns": ["key"],
|
||||
"isUnique": false
|
||||
},
|
||||
"payload_preferences_updated_at_idx": {
|
||||
"name": "payload_preferences_updated_at_idx",
|
||||
"columns": ["updated_at"],
|
||||
"isUnique": false
|
||||
},
|
||||
"payload_preferences_created_at_idx": {
|
||||
"name": "payload_preferences_created_at_idx",
|
||||
"columns": ["created_at"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"payload_preferences_rels": {
|
||||
"name": "payload_preferences_rels",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"order": {
|
||||
"name": "order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"parent_id": {
|
||||
"name": "parent_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"users_id": {
|
||||
"name": "users_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"payload_preferences_rels_order_idx": {
|
||||
"name": "payload_preferences_rels_order_idx",
|
||||
"columns": ["order"],
|
||||
"isUnique": false
|
||||
},
|
||||
"payload_preferences_rels_parent_idx": {
|
||||
"name": "payload_preferences_rels_parent_idx",
|
||||
"columns": ["parent_id"],
|
||||
"isUnique": false
|
||||
},
|
||||
"payload_preferences_rels_path_idx": {
|
||||
"name": "payload_preferences_rels_path_idx",
|
||||
"columns": ["path"],
|
||||
"isUnique": false
|
||||
},
|
||||
"payload_preferences_rels_users_id_idx": {
|
||||
"name": "payload_preferences_rels_users_id_idx",
|
||||
"columns": ["users_id"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"payload_preferences_rels_parent_fk": {
|
||||
"name": "payload_preferences_rels_parent_fk",
|
||||
"tableFrom": "payload_preferences_rels",
|
||||
"tableTo": "payload_preferences",
|
||||
"columnsFrom": ["parent_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"payload_preferences_rels_users_fk": {
|
||||
"name": "payload_preferences_rels_users_fk",
|
||||
"tableFrom": "payload_preferences_rels",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": ["users_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"payload_migrations": {
|
||||
"name": "payload_migrations",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"batch": {
|
||||
"name": "batch",
|
||||
"type": "numeric",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"payload_migrations_updated_at_idx": {
|
||||
"name": "payload_migrations_updated_at_idx",
|
||||
"columns": ["updated_at"],
|
||||
"isUnique": false
|
||||
},
|
||||
"payload_migrations_created_at_idx": {
|
||||
"name": "payload_migrations_created_at_idx",
|
||||
"columns": ["created_at"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
},
|
||||
"id": "2b2f5008-c761-40d0-a858-67d6b4233615",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000"
|
||||
}
|
||||
122
test/database/up-down-migration/migrations/20250328_185055.ts
Normal file
122
test/database/up-down-migration/migrations/20250328_185055.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/db-sqlite'
|
||||
|
||||
import { sql } from '@payloadcms/db-sqlite'
|
||||
|
||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||
await db.run(sql`CREATE TABLE \`users\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`updated_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||
\`created_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||
\`email\` text NOT NULL,
|
||||
\`reset_password_token\` text,
|
||||
\`reset_password_expiration\` text,
|
||||
\`salt\` text,
|
||||
\`hash\` text,
|
||||
\`login_attempts\` numeric DEFAULT 0,
|
||||
\`lock_until\` text
|
||||
);
|
||||
`)
|
||||
await db.run(sql`CREATE INDEX \`users_updated_at_idx\` ON \`users\` (\`updated_at\`);`)
|
||||
await db.run(sql`CREATE INDEX \`users_created_at_idx\` ON \`users\` (\`created_at\`);`)
|
||||
await db.run(sql`CREATE UNIQUE INDEX \`users_email_idx\` ON \`users\` (\`email\`);`)
|
||||
await db.run(sql`CREATE TABLE \`payload_locked_documents\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`global_slug\` text,
|
||||
\`updated_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||
\`created_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL
|
||||
);
|
||||
`)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_locked_documents_global_slug_idx\` ON \`payload_locked_documents\` (\`global_slug\`);`,
|
||||
)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_locked_documents_updated_at_idx\` ON \`payload_locked_documents\` (\`updated_at\`);`,
|
||||
)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_locked_documents_created_at_idx\` ON \`payload_locked_documents\` (\`created_at\`);`,
|
||||
)
|
||||
await db.run(sql`CREATE TABLE \`payload_locked_documents_rels\` (
|
||||
\`id\` integer PRIMARY KEY NOT NULL,
|
||||
\`order\` integer,
|
||||
\`parent_id\` integer NOT NULL,
|
||||
\`path\` text NOT NULL,
|
||||
\`users_id\` integer,
|
||||
FOREIGN KEY (\`parent_id\`) REFERENCES \`payload_locked_documents\`(\`id\`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (\`users_id\`) REFERENCES \`users\`(\`id\`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
`)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_locked_documents_rels_order_idx\` ON \`payload_locked_documents_rels\` (\`order\`);`,
|
||||
)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_locked_documents_rels_parent_idx\` ON \`payload_locked_documents_rels\` (\`parent_id\`);`,
|
||||
)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_locked_documents_rels_path_idx\` ON \`payload_locked_documents_rels\` (\`path\`);`,
|
||||
)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_locked_documents_rels_users_id_idx\` ON \`payload_locked_documents_rels\` (\`users_id\`);`,
|
||||
)
|
||||
await db.run(sql`CREATE TABLE \`payload_preferences\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`key\` text,
|
||||
\`value\` text,
|
||||
\`updated_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||
\`created_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL
|
||||
);
|
||||
`)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_preferences_key_idx\` ON \`payload_preferences\` (\`key\`);`,
|
||||
)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_preferences_updated_at_idx\` ON \`payload_preferences\` (\`updated_at\`);`,
|
||||
)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_preferences_created_at_idx\` ON \`payload_preferences\` (\`created_at\`);`,
|
||||
)
|
||||
await db.run(sql`CREATE TABLE \`payload_preferences_rels\` (
|
||||
\`id\` integer PRIMARY KEY NOT NULL,
|
||||
\`order\` integer,
|
||||
\`parent_id\` integer NOT NULL,
|
||||
\`path\` text NOT NULL,
|
||||
\`users_id\` integer,
|
||||
FOREIGN KEY (\`parent_id\`) REFERENCES \`payload_preferences\`(\`id\`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (\`users_id\`) REFERENCES \`users\`(\`id\`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
`)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_preferences_rels_order_idx\` ON \`payload_preferences_rels\` (\`order\`);`,
|
||||
)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_preferences_rels_parent_idx\` ON \`payload_preferences_rels\` (\`parent_id\`);`,
|
||||
)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_preferences_rels_path_idx\` ON \`payload_preferences_rels\` (\`path\`);`,
|
||||
)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_preferences_rels_users_id_idx\` ON \`payload_preferences_rels\` (\`users_id\`);`,
|
||||
)
|
||||
await db.run(sql`CREATE TABLE \`payload_migrations\` (
|
||||
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
\`name\` text,
|
||||
\`batch\` numeric,
|
||||
\`updated_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||
\`created_at\` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL
|
||||
);
|
||||
`)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_migrations_updated_at_idx\` ON \`payload_migrations\` (\`updated_at\`);`,
|
||||
)
|
||||
await db.run(
|
||||
sql`CREATE INDEX \`payload_migrations_created_at_idx\` ON \`payload_migrations\` (\`created_at\`);`,
|
||||
)
|
||||
}
|
||||
|
||||
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
||||
await db.run(sql`DROP TABLE \`users\`;`)
|
||||
await db.run(sql`DROP TABLE \`payload_locked_documents\`;`)
|
||||
await db.run(sql`DROP TABLE \`payload_locked_documents_rels\`;`)
|
||||
await db.run(sql`DROP TABLE \`payload_preferences\`;`)
|
||||
await db.run(sql`DROP TABLE \`payload_preferences_rels\`;`)
|
||||
await db.run(sql`DROP TABLE \`payload_migrations\`;`)
|
||||
}
|
||||
9
test/database/up-down-migration/migrations/index.ts
Normal file
9
test/database/up-down-migration/migrations/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as migration_20250328_185055 from './20250328_185055.js'
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
up: migration_20250328_185055.up,
|
||||
down: migration_20250328_185055.down,
|
||||
name: '20250328_185055',
|
||||
},
|
||||
]
|
||||
@@ -70,6 +70,7 @@ export const testEslintConfig = [
|
||||
'saveDocAndAssert',
|
||||
'runFilterOptionsTest',
|
||||
'assertNetworkRequests',
|
||||
'assertRequestBody',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -332,7 +332,7 @@ describe('Relationship Field', () => {
|
||||
// now ensure that the same filter options are applied in the list view
|
||||
await page.goto(url.list)
|
||||
|
||||
const whereBuilder = await addListFilter({
|
||||
const { whereBuilder } = await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Relationship Filtered By Field',
|
||||
operatorLabel: 'equals',
|
||||
@@ -367,7 +367,7 @@ describe('Relationship Field', () => {
|
||||
// now ensure that the same filter options are applied in the list view
|
||||
await page.goto(url.list)
|
||||
|
||||
const whereBuilder = await addListFilter({
|
||||
const { whereBuilder } = await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Collapsible > Nested Relationship Filtered By Field',
|
||||
operatorLabel: 'equals',
|
||||
|
||||
@@ -114,7 +114,6 @@ describe('Array', () => {
|
||||
await expect(page.locator('#field-customArrayField__0__text')).toBeVisible()
|
||||
})
|
||||
|
||||
|
||||
test('should bypass min rows validation when no rows present and field is not required', async () => {
|
||||
await page.goto(url.create)
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { openBlocksDrawer } from 'helpers/e2e/openBlocksDrawer.js'
|
||||
import { reorderBlocks } from 'helpers/e2e/reorderBlocks.js'
|
||||
import { scrollEntirePage } from 'helpers/e2e/scrollEntirePage.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import {
|
||||
@@ -312,6 +313,8 @@ describe('Block fields', () => {
|
||||
'Second block',
|
||||
)
|
||||
|
||||
await wait(1000)
|
||||
|
||||
await reorderBlocks({
|
||||
page,
|
||||
fieldName: 'blocks',
|
||||
|
||||
@@ -100,6 +100,7 @@ describe('Radio', () => {
|
||||
await assertToastErrors({
|
||||
page,
|
||||
errors: ['uniqueText'],
|
||||
dismissAfterAssertion: true,
|
||||
})
|
||||
|
||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('create')
|
||||
@@ -113,7 +114,9 @@ describe('Radio', () => {
|
||||
// nested in a group error
|
||||
await page.locator('#field-group__unique').fill(uniqueText)
|
||||
|
||||
await wait(1000)
|
||||
// TODO: used because otherwise the toast locator resolves to 2 items
|
||||
// at the same time. Instead we should uniquely identify each toast.
|
||||
await wait(2000)
|
||||
|
||||
// attempt to save
|
||||
await page.locator('#action-save').click()
|
||||
|
||||
@@ -1507,6 +1507,24 @@ describe('lexicalBlocks', () => {
|
||||
),
|
||||
).toHaveText('Some Description')
|
||||
})
|
||||
|
||||
test('ensure individual inline blocks in lexical editor within a block have initial state on initial load', async () => {
|
||||
await page.goto('http://localhost:3000/admin/collections/LexicalInBlock?limit=10')
|
||||
|
||||
await assertNetworkRequests(
|
||||
page,
|
||||
'/collections/LexicalInBlock/',
|
||||
async () => {
|
||||
await page.locator('.cell-id a').first().click()
|
||||
await page.waitForURL(`**/collections/LexicalInBlock/**`)
|
||||
|
||||
await expect(
|
||||
page.locator('.inline-block:has-text("Inline Block In Lexical")'),
|
||||
).toHaveCount(20)
|
||||
},
|
||||
{ allowedNumberOfRequests: 1 },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('inline blocks', () => {
|
||||
|
||||
@@ -1135,6 +1135,33 @@ describe('lexicalMain', () => {
|
||||
await expect(urlInput).toBeVisible()
|
||||
})
|
||||
|
||||
test('ensure link drawer displays nested block fields if document does not have `create` permission', async () => {
|
||||
await navigateToLexicalFields(true, 'lexical-access-control')
|
||||
const richTextField = page.locator('.rich-text-lexical').first()
|
||||
await richTextField.scrollIntoViewIfNeeded()
|
||||
await expect(richTextField).toBeVisible()
|
||||
|
||||
const link = richTextField.locator('.LexicalEditorTheme__link').first()
|
||||
await link.scrollIntoViewIfNeeded()
|
||||
await expect(link).toBeVisible()
|
||||
await link.click({
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true,
|
||||
button: 'left',
|
||||
})
|
||||
|
||||
await expect(page.locator('.link-edit')).toBeVisible()
|
||||
await page.locator('.link-edit').click()
|
||||
|
||||
const linkDrawer = page.locator('dialog[id^=drawer_1_lexical-rich-text-link-]').first()
|
||||
await expect(linkDrawer).toBeVisible()
|
||||
|
||||
const blockTextInput = linkDrawer.locator('#field-blocks__0__text').first()
|
||||
|
||||
await expect(blockTextInput).toBeVisible()
|
||||
await expect(blockTextInput).toBeEditable()
|
||||
})
|
||||
|
||||
test('lexical cursor / selection should be preserved when swapping upload field and clicking within with its list drawer', async () => {
|
||||
await navigateToLexicalFields()
|
||||
const richTextField = page.locator('.rich-text-lexical').first()
|
||||
@@ -1494,6 +1521,7 @@ describe('lexicalMain', () => {
|
||||
expect(htmlContent).not.toContain('Cargando...')
|
||||
expect(htmlContent).toContain('Start typing, or press')
|
||||
})
|
||||
// eslint-disable-next-line playwright/expect-expect, playwright/no-skipped-test
|
||||
test.skip('ensure simple localized lexical field works', async () => {
|
||||
await navigateToLexicalFields(true, 'lexical-localized-fields')
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { defaultEditorFeatures, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { defaultEditorFeatures, lexicalEditor, LinkFeature } from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { lexicalAccessControlSlug } from '../../slugs.js'
|
||||
|
||||
@@ -22,7 +22,29 @@ export const LexicalAccessControl: CollectionConfig = {
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: [...defaultEditorFeatures],
|
||||
features: [
|
||||
...defaultEditorFeatures,
|
||||
LinkFeature({
|
||||
fields: ({ defaultFields }) => [
|
||||
...defaultFields,
|
||||
{
|
||||
name: 'blocks',
|
||||
type: 'blocks',
|
||||
blocks: [
|
||||
{
|
||||
slug: 'block',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -42,6 +42,23 @@ export const LexicalInBlock: CollectionConfig = {
|
||||
{
|
||||
name: 'lexical',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: [
|
||||
BlocksFeature({
|
||||
inlineBlocks: [
|
||||
{
|
||||
slug: 'inlineBlockInLexical',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -79,7 +79,7 @@ describe('Text', () => {
|
||||
await expect(page.locator('.cell-hiddenTextField')).toBeHidden()
|
||||
await expect(page.locator('#heading-hiddenTextField')).toBeHidden()
|
||||
|
||||
const columnContainer = await openListColumns(page, {})
|
||||
const { columnContainer } = await openListColumns(page, {})
|
||||
|
||||
await expect(
|
||||
columnContainer.locator('.column-selector__column', {
|
||||
@@ -105,7 +105,7 @@ describe('Text', () => {
|
||||
await expect(page.locator('.cell-disabledTextField')).toBeHidden()
|
||||
await expect(page.locator('#heading-disabledTextField')).toBeHidden()
|
||||
|
||||
const columnContainer = await openListColumns(page, {})
|
||||
const { columnContainer } = await openListColumns(page, {})
|
||||
|
||||
await expect(
|
||||
columnContainer.locator('.column-selector__column', {
|
||||
@@ -133,7 +133,7 @@ describe('Text', () => {
|
||||
await expect(page.locator('.cell-adminHiddenTextField').first()).toBeVisible()
|
||||
await expect(page.locator('#heading-adminHiddenTextField')).toBeVisible()
|
||||
|
||||
const columnContainer = await openListColumns(page, {})
|
||||
const { columnContainer } = await openListColumns(page, {})
|
||||
|
||||
await expect(
|
||||
columnContainer.locator('.column-selector__column', {
|
||||
|
||||
@@ -54,6 +54,7 @@ export type SupportedTimezones =
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
|
||||
@@ -511,6 +511,16 @@ export const seed = async (_payload: Payload) => {
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
const getInlineBlock = () => ({
|
||||
type: 'inlineBlock',
|
||||
fields: {
|
||||
id: Math.random().toString(36).substring(2, 15),
|
||||
text: 'text',
|
||||
blockType: 'inlineBlockInLexical',
|
||||
},
|
||||
version: 1,
|
||||
})
|
||||
|
||||
await _payload.create({
|
||||
collection: 'LexicalInBlock',
|
||||
depth: 0,
|
||||
@@ -548,6 +558,32 @@ export const seed = async (_payload: Payload) => {
|
||||
blockName: '2',
|
||||
lexical: textToLexicalJSON({ text: '2' }),
|
||||
},
|
||||
{
|
||||
blockType: 'lexicalInBlock2',
|
||||
lexical: {
|
||||
root: {
|
||||
children: [
|
||||
{
|
||||
children: [...Array.from({ length: 20 }, () => getInlineBlock())],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'root',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
id: '67e1af0b78de3228e23ef1d5',
|
||||
blockName: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -555,7 +591,67 @@ export const seed = async (_payload: Payload) => {
|
||||
await _payload.create({
|
||||
collection: 'lexical-access-control',
|
||||
data: {
|
||||
richText: textToLexicalJSON({ text: 'text' }),
|
||||
richText: {
|
||||
root: {
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'text ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'link',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'link',
|
||||
version: 3,
|
||||
fields: {
|
||||
url: 'https://',
|
||||
newTab: false,
|
||||
linkType: 'custom',
|
||||
blocks: [
|
||||
{
|
||||
id: '67e45673cbd5181ca8cbeef7',
|
||||
blockType: 'block',
|
||||
},
|
||||
],
|
||||
},
|
||||
id: '67e4566fcbd5181ca8cbeef5',
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'root',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
title: 'title',
|
||||
},
|
||||
depth: 0,
|
||||
|
||||
5
test/form-state/collections/Posts/ArrayRowLabel.tsx
Normal file
5
test/form-state/collections/Posts/ArrayRowLabel.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
export const ArrayRowLabel = () => {
|
||||
return <p>This is a custom component</p>
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
|
||||
export const PostsCollection: CollectionConfig = {
|
||||
@@ -64,5 +66,21 @@ export const PostsCollection: CollectionConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
admin: {
|
||||
components: {
|
||||
RowLabel: './collections/Posts/ArrayRowLabel.js#ArrayRowLabel',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { BrowserContext, Page } from '@playwright/test'
|
||||
import type { PayloadTestSDK } from 'helpers/sdk/index.js'
|
||||
import type { FormState } from 'payload'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { addBlock } from 'helpers/e2e/addBlock.js'
|
||||
import { assertNetworkRequests } from 'helpers/e2e/assertNetworkRequests.js'
|
||||
import { assertRequestBody } from 'helpers/e2e/assertRequestBody.js'
|
||||
import * as path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
@@ -17,7 +19,8 @@ import {
|
||||
} from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
import { TEST_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
import { postsSlug } from './collections/Posts/index.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
@@ -34,7 +37,7 @@ test.describe('Form State', () => {
|
||||
test.beforeAll(async ({ browser }, testInfo) => {
|
||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||
;({ payload, serverURL } = await initPayloadE2ENoConfig({ dirname }))
|
||||
postsUrl = new AdminUrlUtil(serverURL, 'posts')
|
||||
postsUrl = new AdminUrlUtil(serverURL, postsSlug)
|
||||
|
||||
context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
@@ -144,6 +147,64 @@ test.describe('Form State', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('should send `lastRenderedPath` only when necessary', async () => {
|
||||
await page.goto(postsUrl.create)
|
||||
const field = page.locator('#field-title')
|
||||
await field.fill('Test')
|
||||
|
||||
// The `array` itself SHOULD have a `lastRenderedPath` because it was rendered on initial load
|
||||
await assertRequestBody<{ args: { formState: FormState } }[]>(page, {
|
||||
action: await page.locator('#field-array .array-field__add-row').click(),
|
||||
url: postsUrl.create,
|
||||
expect: (body) =>
|
||||
Boolean(
|
||||
body?.[0]?.args?.formState?.['array'] && body[0].args.formState['array'].lastRenderedPath,
|
||||
),
|
||||
})
|
||||
|
||||
await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url() === postsUrl.create &&
|
||||
response.status() === 200 &&
|
||||
response.headers()['content-type'] === 'text/x-component',
|
||||
)
|
||||
|
||||
// The `array` itself SHOULD still have a `lastRenderedPath`
|
||||
// The rich text field in the first row SHOULD ALSO have a `lastRenderedPath` bc it was rendered in the first request
|
||||
await assertRequestBody<{ args: { formState: FormState } }[]>(page, {
|
||||
action: await page.locator('#field-array .array-field__add-row').click(),
|
||||
url: postsUrl.create,
|
||||
expect: (body) =>
|
||||
Boolean(
|
||||
body?.[0]?.args?.formState?.['array'] &&
|
||||
body[0].args.formState['array'].lastRenderedPath &&
|
||||
body[0].args.formState['array.0.richText']?.lastRenderedPath,
|
||||
),
|
||||
})
|
||||
|
||||
await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url() === postsUrl.create &&
|
||||
response.status() === 200 &&
|
||||
response.headers()['content-type'] === 'text/x-component',
|
||||
)
|
||||
|
||||
// The `array` itself SHOULD still have a `lastRenderedPath`
|
||||
// The rich text field in the first row SHOULD ALSO have a `lastRenderedPath` bc it was rendered in the first request
|
||||
// The rich text field in the second row SHOULD ALSO have a `lastRenderedPath` bc it was rendered in the second request
|
||||
await assertRequestBody<{ args: { formState: FormState } }[]>(page, {
|
||||
action: await page.locator('#field-array .array-field__add-row').click(),
|
||||
url: postsUrl.create,
|
||||
expect: (body) =>
|
||||
Boolean(
|
||||
body?.[0]?.args?.formState?.['array'] &&
|
||||
body[0].args.formState['array'].lastRenderedPath &&
|
||||
body[0].args.formState['array.0.richText']?.lastRenderedPath &&
|
||||
body[0].args.formState['array.1.richText']?.lastRenderedPath,
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
test('should queue onChange functions', async () => {
|
||||
await page.goto(postsUrl.create)
|
||||
const field = page.locator('#field-title')
|
||||
@@ -179,6 +240,106 @@ test.describe('Form State', () => {
|
||||
|
||||
await cdpSession.detach()
|
||||
})
|
||||
|
||||
test('should not cause nested custom components to disappear when adding a row then editing a field', async () => {
|
||||
await page.goto(postsUrl.create)
|
||||
const field = page.locator('#field-title')
|
||||
await field.fill('Test')
|
||||
|
||||
const cdpSession = await throttleTest({
|
||||
page,
|
||||
context,
|
||||
delay: 'Slow 3G',
|
||||
})
|
||||
|
||||
await assertNetworkRequests(
|
||||
page,
|
||||
postsUrl.create,
|
||||
async () => {
|
||||
await page.locator('#field-array .array-field__add-row').click()
|
||||
await page.locator('#field-title').fill('Test 2')
|
||||
|
||||
// use `waitForSelector` to ensure the element doesn't appear and then disappear
|
||||
// eslint-disable-next-line playwright/no-wait-for-selector
|
||||
await page.waitForSelector('#field-array #array-row-0 .field-type.rich-text-lexical', {
|
||||
timeout: TEST_TIMEOUT,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.locator('#field-array #array-row-0 .field-type.rich-text-lexical'),
|
||||
).toBeVisible()
|
||||
},
|
||||
{
|
||||
allowedNumberOfRequests: 2,
|
||||
timeout: 10000,
|
||||
},
|
||||
)
|
||||
|
||||
await cdpSession.send('Network.emulateNetworkConditions', {
|
||||
offline: false,
|
||||
latency: 0,
|
||||
downloadThroughput: -1,
|
||||
uploadThroughput: -1,
|
||||
})
|
||||
|
||||
await cdpSession.detach()
|
||||
})
|
||||
|
||||
test('should not cause nested custom components to disappear when adding rows back-to-back', async () => {
|
||||
await page.goto(postsUrl.create)
|
||||
const field = page.locator('#field-title')
|
||||
await field.fill('Test')
|
||||
|
||||
const cdpSession = await throttleTest({
|
||||
page,
|
||||
context,
|
||||
delay: 'Slow 3G',
|
||||
})
|
||||
|
||||
// Add two rows quickly
|
||||
// Test that the rich text fields within the rows do not disappear
|
||||
await assertNetworkRequests(
|
||||
page,
|
||||
postsUrl.create,
|
||||
async () => {
|
||||
await page.locator('#field-array .array-field__add-row').click()
|
||||
await page.locator('#field-array .array-field__add-row').click()
|
||||
|
||||
// use `waitForSelector` to ensure the element doesn't appear and then disappear
|
||||
// eslint-disable-next-line playwright/no-wait-for-selector
|
||||
await page.waitForSelector('#field-array #array-row-0 .field-type.rich-text-lexical', {
|
||||
timeout: TEST_TIMEOUT,
|
||||
})
|
||||
|
||||
// use `waitForSelector` to ensure the element doesn't appear and then disappear
|
||||
// eslint-disable-next-line playwright/no-wait-for-selector
|
||||
await page.waitForSelector('#field-array #array-row-1 .field-type.rich-text-lexical', {
|
||||
timeout: TEST_TIMEOUT,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.locator('#field-array #array-row-0 .field-type.rich-text-lexical'),
|
||||
).toBeVisible()
|
||||
|
||||
await expect(
|
||||
page.locator('#field-array #array-row-1 .field-type.rich-text-lexical'),
|
||||
).toBeVisible()
|
||||
},
|
||||
{
|
||||
allowedNumberOfRequests: 2,
|
||||
timeout: 10000,
|
||||
},
|
||||
)
|
||||
|
||||
await cdpSession.send('Network.emulateNetworkConditions', {
|
||||
offline: false,
|
||||
latency: 0,
|
||||
downloadThroughput: -1,
|
||||
uploadThroughput: -1,
|
||||
})
|
||||
|
||||
await cdpSession.detach()
|
||||
})
|
||||
})
|
||||
|
||||
async function createPost(overrides?: Partial<Post>): Promise<Post> {
|
||||
|
||||
@@ -21,11 +21,8 @@ const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
describe('Form State', () => {
|
||||
// --__--__--__--__--__--__--__--__--__
|
||||
// Boilerplate test setup/teardown
|
||||
// --__--__--__--__--__--__--__--__--__
|
||||
beforeAll(async () => {
|
||||
;({ payload, restClient } = await initPayloadInt(dirname))
|
||||
;({ payload, restClient } = await initPayloadInt(dirname, undefined, true))
|
||||
|
||||
const data = await restClient
|
||||
.POST('/users/login', {
|
||||
@@ -57,6 +54,7 @@ describe('Form State', () => {
|
||||
})
|
||||
|
||||
const { state } = await buildFormState({
|
||||
mockRSCs: true,
|
||||
id: postData.id,
|
||||
collectionSlug: postsSlug,
|
||||
data: postData,
|
||||
@@ -95,7 +93,6 @@ describe('Form State', () => {
|
||||
validateUsingEvent: {},
|
||||
blocks: {
|
||||
initialValue: 0,
|
||||
requiresRender: false,
|
||||
rows: [],
|
||||
value: 0,
|
||||
},
|
||||
@@ -113,6 +110,7 @@ describe('Form State', () => {
|
||||
})
|
||||
|
||||
const { state } = await buildFormState({
|
||||
mockRSCs: true,
|
||||
id: postData.id,
|
||||
collectionSlug: postsSlug,
|
||||
data: postData,
|
||||
@@ -134,9 +132,89 @@ describe('Form State', () => {
|
||||
title: {
|
||||
value: postData.title,
|
||||
initialValue: postData.title,
|
||||
lastRenderedPath: 'title',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it.todo('should skip validation if specified')
|
||||
it('should not render custom components when `lastRenderedPath` exists', async () => {
|
||||
const req = await createLocalReq({ user }, payload)
|
||||
|
||||
const { state: stateWithRow } = await buildFormState({
|
||||
mockRSCs: true,
|
||||
collectionSlug: postsSlug,
|
||||
formState: {
|
||||
array: {
|
||||
rows: [
|
||||
{
|
||||
id: '123',
|
||||
},
|
||||
],
|
||||
},
|
||||
'array.0.id': {
|
||||
value: '123',
|
||||
initialValue: '123',
|
||||
},
|
||||
},
|
||||
docPermissions: undefined,
|
||||
docPreferences: {
|
||||
fields: {},
|
||||
},
|
||||
documentFormState: undefined,
|
||||
operation: 'update',
|
||||
renderAllFields: false,
|
||||
req,
|
||||
schemaPath: postsSlug,
|
||||
})
|
||||
|
||||
// Ensure that row 1 _DOES_ return with rendered components
|
||||
expect(stateWithRow?.['array.0.richText']?.lastRenderedPath).toStrictEqual('array.0.richText')
|
||||
expect(stateWithRow?.['array.0.richText']?.customComponents?.Field).toBeDefined()
|
||||
|
||||
const { state: stateWithTitle } = await buildFormState({
|
||||
mockRSCs: true,
|
||||
collectionSlug: postsSlug,
|
||||
formState: {
|
||||
array: {
|
||||
rows: [
|
||||
{
|
||||
id: '123',
|
||||
},
|
||||
{
|
||||
id: '456',
|
||||
},
|
||||
],
|
||||
},
|
||||
'array.0.id': {
|
||||
value: '123',
|
||||
initialValue: '123',
|
||||
},
|
||||
'array.0.richText': {
|
||||
lastRenderedPath: 'array.0.richText',
|
||||
},
|
||||
'array.1.id': {
|
||||
value: '456',
|
||||
initialValue: '456',
|
||||
},
|
||||
},
|
||||
docPermissions: undefined,
|
||||
docPreferences: {
|
||||
fields: {},
|
||||
},
|
||||
documentFormState: undefined,
|
||||
operation: 'update',
|
||||
renderAllFields: false,
|
||||
schemaPath: postsSlug,
|
||||
req,
|
||||
})
|
||||
|
||||
// Ensure that row 1 _DOES NOT_ return with rendered components
|
||||
expect(stateWithTitle?.['array.0.richText']).toHaveProperty('lastRenderedPath')
|
||||
expect(stateWithTitle?.['array.0.richText']).not.toHaveProperty('customComponents')
|
||||
|
||||
// Ensure that row 2 _DOES_ return with rendered components
|
||||
expect(stateWithTitle?.['array.1.richText']).toHaveProperty('lastRenderedPath')
|
||||
expect(stateWithTitle?.['array.1.richText']).toHaveProperty('customComponents')
|
||||
expect(stateWithTitle?.['array.1.richText']?.customComponents?.Field).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -54,6 +54,7 @@ export type SupportedTimezones =
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
@@ -140,6 +141,26 @@ export interface Post {
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
array?:
|
||||
| {
|
||||
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;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -243,6 +264,12 @@ export interface PostsSelect<T extends boolean = true> {
|
||||
blockName?: T;
|
||||
};
|
||||
};
|
||||
array?:
|
||||
| T
|
||||
| {
|
||||
richText?: T;
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ export async function saveDocAndAssert(
|
||||
|
||||
if (expectation === 'success') {
|
||||
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).not.toContain('create')
|
||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).not.toContain('/create')
|
||||
} else {
|
||||
await expect(page.locator('.payload-toast-container .toast-error')).toBeVisible()
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import { expect } from '@playwright/test'
|
||||
export async function assertToastErrors({
|
||||
page,
|
||||
errors,
|
||||
dismissAfterAssertion,
|
||||
}: {
|
||||
dismissAfterAssertion?: boolean
|
||||
errors: string[]
|
||||
page: Page
|
||||
}): Promise<void> {
|
||||
@@ -24,4 +26,13 @@ export async function assertToastErrors({
|
||||
).toHaveText(error)
|
||||
}
|
||||
}
|
||||
|
||||
if (dismissAfterAssertion) {
|
||||
const closeButtons = page.locator('.payload-toast-container button.payload-toast-close-button')
|
||||
const count = await closeButtons.count()
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
await closeButtons.nth(i).click()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ export const addListFilter = async ({
|
||||
replaceExisting?: boolean
|
||||
skipValueInput?: boolean
|
||||
value?: string
|
||||
}): Promise<Locator> => {
|
||||
}): Promise<{
|
||||
whereBuilder: Locator
|
||||
}> => {
|
||||
await openListFilters(page, {})
|
||||
|
||||
const whereBuilder = page.locator('.where-builder')
|
||||
@@ -53,5 +55,5 @@ export const addListFilter = async ({
|
||||
}
|
||||
}
|
||||
|
||||
return whereBuilder
|
||||
return { whereBuilder }
|
||||
}
|
||||
|
||||
51
test/helpers/e2e/assertRequestBody.ts
Normal file
51
test/helpers/e2e/assertRequestBody.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* A helper function to assert the body of a network request.
|
||||
* This is useful for reading the body of a request and testing whether it is correct.
|
||||
* For example, if you have a form that submits data to an API, you can use this function to
|
||||
* assert that the data being sent is correct.
|
||||
* @param page The Playwright page
|
||||
* @param options Options
|
||||
* @param options.action The action to perform that will trigger the request
|
||||
* @param options.expect A function to run after the request is made to assert the request body
|
||||
* @returns The request body
|
||||
* @example
|
||||
* const requestBody = await assertRequestBody(page, {
|
||||
* action: page.click('button'),
|
||||
* expect: (requestBody) => expect(requestBody.foo).toBe('bar')
|
||||
* })
|
||||
*/
|
||||
export const assertRequestBody = async <T>(
|
||||
page: Page,
|
||||
options: {
|
||||
action: Promise<void> | void
|
||||
expect?: (requestBody: T) => boolean | Promise<boolean>
|
||||
requestMethod?: string
|
||||
url: string
|
||||
},
|
||||
): Promise<T | undefined> => {
|
||||
const [request] = await Promise.all([
|
||||
page.waitForRequest((request) =>
|
||||
Boolean(
|
||||
request.url().startsWith(options.url) &&
|
||||
(request.method() === options.requestMethod || 'POST'),
|
||||
),
|
||||
),
|
||||
await options.action,
|
||||
])
|
||||
|
||||
const requestBody = request.postData()
|
||||
|
||||
if (typeof requestBody === 'string') {
|
||||
const parsedBody = JSON.parse(requestBody) as T
|
||||
|
||||
if (typeof options.expect === 'function') {
|
||||
expect(await options.expect(parsedBody)).toBeTruthy()
|
||||
}
|
||||
|
||||
return parsedBody
|
||||
}
|
||||
}
|
||||
86
test/helpers/e2e/assertResponseBody.ts
Normal file
86
test/helpers/e2e/assertResponseBody.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
function parseRSC(rscText: string) {
|
||||
// Next.js streams use special delimiters like "\n"
|
||||
const chunks = rscText.split('\n').filter((line) => line.trim() !== '')
|
||||
|
||||
// find the chunk starting with '1:', remove the '1:' prefix and parse the rest
|
||||
const match = chunks.find((chunk) => chunk.startsWith('1:'))
|
||||
|
||||
if (match) {
|
||||
const jsonString = match.slice(2).trim()
|
||||
if (jsonString) {
|
||||
try {
|
||||
return JSON.parse(jsonString)
|
||||
} catch (err) {
|
||||
console.error('Failed to parse JSON:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to assert the response of a network request.
|
||||
* This is useful for reading the response of a request and testing whether it is correct.
|
||||
* For example, if you have a form that submits data to an API, you can use this function to
|
||||
* assert that the data sent back is correct.
|
||||
* @param page The Playwright page
|
||||
* @param options Options
|
||||
* @param options.action The action to perform that will trigger the request
|
||||
* @param options.expect A function to run after the request is made to assert the response body
|
||||
* @param options.url The URL to match in the network requests
|
||||
* @returns The request body
|
||||
* @example
|
||||
* const responseBody = await assertResponseBody(page, {
|
||||
* action: page.click('button'),
|
||||
* expect: (responseBody) => expect(responseBody.foo).toBe('bar')
|
||||
* })
|
||||
*/
|
||||
export const assertResponseBody = async <T>(
|
||||
page: Page,
|
||||
options: {
|
||||
action: Promise<void> | void
|
||||
expect?: (requestBody: T) => boolean | Promise<boolean>
|
||||
requestMethod?: string
|
||||
responseContentType?: string
|
||||
url?: string
|
||||
},
|
||||
): Promise<T | undefined> => {
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse((response) =>
|
||||
Boolean(
|
||||
response.url().includes(options.url || '') &&
|
||||
response.status() === 200 &&
|
||||
response
|
||||
.headers()
|
||||
['content-type']?.includes(options.responseContentType || 'application/json'),
|
||||
),
|
||||
),
|
||||
await options.action,
|
||||
])
|
||||
|
||||
if (!response) {
|
||||
throw new Error('No response received')
|
||||
}
|
||||
|
||||
const responseBody = await response.text()
|
||||
const responseType = response.headers()['content-type']?.split(';')[0]
|
||||
|
||||
let parsedBody: T = undefined as T
|
||||
|
||||
if (responseType === 'text/x-component') {
|
||||
parsedBody = parseRSC(responseBody)
|
||||
} else if (typeof responseBody === 'string') {
|
||||
parsedBody = JSON.parse(responseBody) as T
|
||||
}
|
||||
|
||||
if (typeof options.expect === 'function') {
|
||||
expect(await options.expect(parsedBody)).toBeTruthy()
|
||||
}
|
||||
|
||||
return parsedBody
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
@@ -11,7 +11,9 @@ export const openListColumns = async (
|
||||
columnContainerSelector?: string
|
||||
togglerSelector?: string
|
||||
},
|
||||
): Promise<any> => {
|
||||
): Promise<{
|
||||
columnContainer: Locator
|
||||
}> => {
|
||||
const columnContainer = page.locator(columnContainerSelector).first()
|
||||
|
||||
const isAlreadyOpen = await columnContainer.isVisible()
|
||||
@@ -22,5 +24,5 @@ export const openListColumns = async (
|
||||
|
||||
await expect(page.locator(`${columnContainerSelector}.rah-static--height-auto`)).toBeVisible()
|
||||
|
||||
return columnContainer
|
||||
return { columnContainer }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
@@ -11,10 +11,12 @@ export const openListFilters = async (
|
||||
filterContainerSelector?: string
|
||||
togglerSelector?: string
|
||||
},
|
||||
) => {
|
||||
const columnContainer = page.locator(filterContainerSelector).first()
|
||||
): Promise<{
|
||||
filterContainer: Locator
|
||||
}> => {
|
||||
const filterContainer = page.locator(filterContainerSelector).first()
|
||||
|
||||
const isAlreadyOpen = await columnContainer.isVisible()
|
||||
const isAlreadyOpen = await filterContainer.isVisible()
|
||||
|
||||
if (!isAlreadyOpen) {
|
||||
await page.locator(togglerSelector).first().click()
|
||||
@@ -22,5 +24,5 @@ export const openListFilters = async (
|
||||
|
||||
await expect(page.locator(`${filterContainerSelector}.rah-static--height-auto`)).toBeVisible()
|
||||
|
||||
return columnContainer
|
||||
return { filterContainer }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
@@ -22,8 +22,13 @@ export const toggleColumn = async (
|
||||
targetState?: 'off' | 'on'
|
||||
togglerSelector?: string
|
||||
},
|
||||
): Promise<any> => {
|
||||
const columnContainer = await openListColumns(page, { togglerSelector, columnContainerSelector })
|
||||
): Promise<{
|
||||
columnContainer: Locator
|
||||
}> => {
|
||||
const { columnContainer } = await openListColumns(page, {
|
||||
togglerSelector,
|
||||
columnContainerSelector,
|
||||
})
|
||||
|
||||
const column = columnContainer.locator(`.column-selector .column-selector__column`, {
|
||||
hasText: exactText(columnLabel),
|
||||
@@ -57,7 +62,7 @@ export const toggleColumn = async (
|
||||
await waitForColumnInURL({ page, columnName, state: targetState })
|
||||
}
|
||||
|
||||
return column
|
||||
return { columnContainer }
|
||||
}
|
||||
|
||||
export const waitForColumnInURL = async ({
|
||||
|
||||
26
test/helpers/e2e/toggleListMenu.ts
Normal file
26
test/helpers/e2e/toggleListMenu.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import { exactText } from 'helpers.js'
|
||||
|
||||
export async function openListMenu({ page }: { page: Page }) {
|
||||
const listMenu = page.locator('#list-menu')
|
||||
await listMenu.locator('button.popup-button').click()
|
||||
await expect(listMenu.locator('.popup__content')).toBeVisible()
|
||||
}
|
||||
|
||||
export async function clickListMenuItem({
|
||||
page,
|
||||
menuItemLabel,
|
||||
}: {
|
||||
menuItemLabel: string
|
||||
page: Page
|
||||
}) {
|
||||
await openListMenu({ page })
|
||||
|
||||
const menuItem = page.locator('.popup__content').locator('button', {
|
||||
hasText: exactText(menuItemLabel),
|
||||
})
|
||||
|
||||
await menuItem.click()
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export type UpdateManyArgs<
|
||||
TSlug extends keyof TGeneratedTypes['collections'],
|
||||
> = {
|
||||
id: never
|
||||
where?: WhereField
|
||||
where?: Where
|
||||
} & UpdateBaseArgs<TGeneratedTypes, TSlug>
|
||||
|
||||
export type UpdateBaseArgs<
|
||||
|
||||
@@ -82,4 +82,27 @@ describe('i18n', () => {
|
||||
page.locator('.componentWithCustomI18n .componentWithCustomI18nCustomValidI18nT'),
|
||||
).toHaveText('My custom translation')
|
||||
})
|
||||
|
||||
test('ensure translations update correctly when switching language', async () => {
|
||||
await page.goto(serverURL + '/admin/account')
|
||||
|
||||
await page.locator('div.rs__control').click()
|
||||
await page.locator('div.rs__option').filter({ hasText: 'English' }).click()
|
||||
await expect(page.locator('div.payload-settings h3')).toHaveText('Payload Settings')
|
||||
|
||||
await page.goto(serverURL + '/admin/collections/collection1/create')
|
||||
await expect(page.locator('label[for="field-fieldDefaultI18nValid"]')).toHaveText(
|
||||
'Add {{label}}',
|
||||
)
|
||||
|
||||
await page.goto(serverURL + '/admin/account')
|
||||
await page.locator('div.rs__control').click()
|
||||
await page.locator('div.rs__option').filter({ hasText: 'Español' }).click()
|
||||
await expect(page.locator('div.payload-settings h3')).toHaveText('Configuración de la carga')
|
||||
|
||||
await page.goto(serverURL + '/admin/collections/collection1/create')
|
||||
await expect(page.locator('label[for="field-fieldDefaultI18nValid"]')).toHaveText(
|
||||
'Añadir {{label}}',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,6 +5,11 @@ export const versionsSlug = 'versions'
|
||||
export const Versions: CollectionConfig = {
|
||||
slug: versionsSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'category',
|
||||
relationTo: 'categories',
|
||||
|
||||
@@ -569,7 +569,7 @@ describe('Joins Field', () => {
|
||||
|
||||
const version = await payload.create({
|
||||
collection: 'versions',
|
||||
data: { categoryVersion: category.id },
|
||||
data: { title: 'version', categoryVersion: category.id },
|
||||
})
|
||||
|
||||
const res = await payload.find({ collection: 'categories-versions', draft: false })
|
||||
@@ -582,7 +582,7 @@ describe('Joins Field', () => {
|
||||
|
||||
const version = await payload.create({
|
||||
collection: 'versions',
|
||||
data: { categoryVersions: [category.id] },
|
||||
data: { title: 'version', categoryVersions: [category.id] },
|
||||
})
|
||||
|
||||
const res = await payload.find({ collection: 'categories-versions', draft: false })
|
||||
@@ -595,7 +595,7 @@ describe('Joins Field', () => {
|
||||
|
||||
const version = await payload.create({
|
||||
collection: 'versions',
|
||||
data: { categoryVersion: category.id },
|
||||
data: { title: 'version', categoryVersion: category.id },
|
||||
})
|
||||
|
||||
const res = await payload.find({
|
||||
@@ -606,12 +606,41 @@ describe('Joins Field', () => {
|
||||
expect(res.docs[0].relatedVersions.docs[0].id).toBe(version.id)
|
||||
})
|
||||
|
||||
it('should populate joins with hasMany when on both sides documents are in draft', async () => {
|
||||
const category = await payload.create({
|
||||
collection: 'categories-versions',
|
||||
data: { _status: 'draft' },
|
||||
draft: true,
|
||||
})
|
||||
|
||||
const version = await payload.create({
|
||||
collection: 'versions',
|
||||
data: { title: 'original-title', _status: 'draft', categoryVersion: category.id },
|
||||
draft: true,
|
||||
})
|
||||
|
||||
await payload.update({
|
||||
collection: 'versions',
|
||||
id: version.id,
|
||||
data: { title: 'updated-title' },
|
||||
draft: true,
|
||||
})
|
||||
|
||||
const res = await payload.find({
|
||||
collection: 'categories-versions',
|
||||
draft: true,
|
||||
})
|
||||
|
||||
expect(res.docs[0].relatedVersions.docs[0].id).toBe(version.id)
|
||||
expect(res.docs[0].relatedVersions.docs[0].title).toBe('updated-title')
|
||||
})
|
||||
|
||||
it('should populate joins when versions on both sides draft true payload.db.queryDrafts', async () => {
|
||||
const category = await payload.create({ collection: 'categories-versions', data: {} })
|
||||
|
||||
const version = await payload.create({
|
||||
collection: 'versions',
|
||||
data: { categoryVersions: [category.id] },
|
||||
data: { categoryVersions: [category.id], title: 'version' },
|
||||
})
|
||||
|
||||
const res = await payload.find({
|
||||
@@ -911,6 +940,52 @@ describe('Joins Field', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should populate joins with hasMany when on both sides documents are in draft', async () => {
|
||||
const category = await payload.create({
|
||||
collection: 'categories-versions',
|
||||
data: { _status: 'draft' },
|
||||
draft: true,
|
||||
})
|
||||
|
||||
const version = await payload.create({
|
||||
collection: 'versions',
|
||||
data: { _status: 'draft', title: 'original-title', categoryVersion: category.id },
|
||||
draft: true,
|
||||
})
|
||||
|
||||
await payload.update({
|
||||
collection: 'versions',
|
||||
draft: true,
|
||||
id: version.id,
|
||||
data: { title: 'updated-title' },
|
||||
})
|
||||
|
||||
const query = `query {
|
||||
CategoriesVersions(draft: true) {
|
||||
docs {
|
||||
relatedVersions(
|
||||
limit: 1
|
||||
) {
|
||||
docs {
|
||||
id,
|
||||
title
|
||||
}
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
const res = await restClient
|
||||
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
||||
.then((res) => res.json())
|
||||
|
||||
expect(res.data.CategoriesVersions.docs[0].relatedVersions.docs[0].id).toBe(version.id)
|
||||
expect(res.data.CategoriesVersions.docs[0].relatedVersions.docs[0].title).toBe(
|
||||
'updated-title',
|
||||
)
|
||||
})
|
||||
|
||||
it('should have simple paginate for joins inside groups', async () => {
|
||||
const queryWithLimit = `query {
|
||||
Categories(where: {
|
||||
|
||||
@@ -54,6 +54,7 @@ export type SupportedTimezones =
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
@@ -464,6 +465,7 @@ export interface Singular {
|
||||
*/
|
||||
export interface Version {
|
||||
id: string;
|
||||
title: string;
|
||||
category?: (string | null) | Category;
|
||||
categoryVersion?: (string | null) | CategoriesVersion;
|
||||
categoryVersions?: (string | CategoriesVersion)[] | null;
|
||||
@@ -999,6 +1001,7 @@ export interface UploadsSelect<T extends boolean = true> {
|
||||
* via the `definition` "versions_select".
|
||||
*/
|
||||
export interface VersionsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
category?: T;
|
||||
categoryVersion?: T;
|
||||
categoryVersions?: T;
|
||||
@@ -1232,7 +1235,6 @@ export interface Auth {
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const pagesSlug = 'pages'
|
||||
import { pagesSlug } from '../../slugs.js'
|
||||
|
||||
export const PagesCollection: CollectionConfig = {
|
||||
slug: pagesSlug,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
import { postsSlug } from '../../slugs.js'
|
||||
|
||||
export const PostsCollection: CollectionConfig = {
|
||||
slug: postsSlug,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { usersSlug } from '../../slugs.js'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
slug: usersSlug,
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
|
||||
@@ -2,13 +2,13 @@ import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser, regularUser } from '../credentials.js'
|
||||
import { PagesCollection, pagesSlug } from './collections/Pages/index.js'
|
||||
import { PostsCollection, postsSlug } from './collections/Posts/index.js'
|
||||
import { PagesCollection } from './collections/Pages/index.js'
|
||||
import { PostsCollection } from './collections/Posts/index.js'
|
||||
import { TestsCollection } from './collections/Tests/index.js'
|
||||
import { Users } from './collections/Users/index.js'
|
||||
import { AdminGlobal } from './globals/Admin/index.js'
|
||||
import { MenuGlobal } from './globals/Menu/index.js'
|
||||
import { seed } from './seed.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
@@ -23,39 +23,7 @@ export default buildConfigWithDefaults({
|
||||
globals: [AdminGlobal, MenuGlobal],
|
||||
onInit: async (payload) => {
|
||||
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
name: 'Admin',
|
||||
roles: ['is_admin', 'is_user'],
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: regularUser.email,
|
||||
password: regularUser.password,
|
||||
name: 'Dev',
|
||||
roles: ['is_user'],
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: pagesSlug,
|
||||
data: {
|
||||
text: 'example page',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
text: 'example post',
|
||||
},
|
||||
})
|
||||
await seed(payload)
|
||||
}
|
||||
},
|
||||
typescript: {
|
||||
|
||||
@@ -5,35 +5,29 @@ import { Locked, NotFound } from 'payload'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||
import type { Menu, Page, Post, User } from './payload-types.js'
|
||||
import type { Post, User } from './payload-types.js'
|
||||
|
||||
import { devUser } from '../credentials.js'
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import { pagesSlug } from './collections/Pages/index.js'
|
||||
import { postsSlug } from './collections/Posts/index.js'
|
||||
import { menuSlug } from './globals/Menu/index.js'
|
||||
import { pagesSlug, postsSlug } from './slugs.js'
|
||||
|
||||
const lockedDocumentCollection = 'payload-locked-documents'
|
||||
|
||||
let payload: Payload
|
||||
let token: string
|
||||
let restClient: NextRESTClient
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
describe('Locked documents', () => {
|
||||
let post: Post
|
||||
let page: Page
|
||||
let menu: Menu
|
||||
let user: any
|
||||
let user2: any
|
||||
let postConfig: SanitizedCollectionConfig
|
||||
|
||||
beforeAll(async () => {
|
||||
// @ts-expect-error: initPayloadInt does not have a proper type definition
|
||||
;({ payload, restClient } = await initPayloadInt(dirname))
|
||||
;({ payload } = await initPayloadInt(dirname))
|
||||
|
||||
postConfig = payload.config.collections.find(
|
||||
({ slug }) => slug === postsSlug,
|
||||
@@ -49,8 +43,6 @@ describe('Locked documents', () => {
|
||||
|
||||
user = loginResult.user
|
||||
|
||||
token = loginResult.token as string
|
||||
|
||||
user2 = await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
@@ -66,14 +58,14 @@ describe('Locked documents', () => {
|
||||
},
|
||||
})
|
||||
|
||||
page = await payload.create({
|
||||
await payload.create({
|
||||
collection: pagesSlug,
|
||||
data: {
|
||||
text: 'some page',
|
||||
},
|
||||
})
|
||||
|
||||
menu = await payload.updateGlobal({
|
||||
await payload.updateGlobal({
|
||||
slug: menuSlug,
|
||||
data: {
|
||||
globalText: 'global text',
|
||||
|
||||
57
test/locked-documents/seed.ts
Normal file
57
test/locked-documents/seed.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import { devUser, regularUser } from '../credentials.js'
|
||||
import { executePromises } from '../helpers/executePromises.js'
|
||||
import { seedDB } from '../helpers/seed.js'
|
||||
import { collectionSlugs, pagesSlug, postsSlug } from './slugs.js'
|
||||
|
||||
export const seed = async (_payload: Payload) => {
|
||||
await executePromises(
|
||||
[
|
||||
() =>
|
||||
_payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
name: 'Admin',
|
||||
roles: ['is_admin', 'is_user'],
|
||||
},
|
||||
}),
|
||||
() =>
|
||||
_payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: regularUser.email,
|
||||
password: regularUser.password,
|
||||
name: 'Dev',
|
||||
roles: ['is_user'],
|
||||
},
|
||||
}),
|
||||
() =>
|
||||
_payload.create({
|
||||
collection: pagesSlug,
|
||||
data: {
|
||||
text: 'example page',
|
||||
},
|
||||
}),
|
||||
() =>
|
||||
_payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
text: 'example post',
|
||||
},
|
||||
}),
|
||||
],
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
export async function clearAndSeedEverything(_payload: Payload) {
|
||||
return await seedDB({
|
||||
_payload,
|
||||
collectionSlugs,
|
||||
seedFunction: seed,
|
||||
snapshotKey: 'adminTests',
|
||||
})
|
||||
}
|
||||
7
test/locked-documents/slugs.ts
Normal file
7
test/locked-documents/slugs.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const pagesSlug = 'pages'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
|
||||
export const usersSlug = 'users'
|
||||
|
||||
export const collectionSlugs = [pagesSlug, postsSlug, usersSlug]
|
||||
2
test/query-presets/.gitignore
vendored
Normal file
2
test/query-presets/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/media
|
||||
/media-gif
|
||||
21
test/query-presets/collections/Pages/index.ts
Normal file
21
test/query-presets/collections/Pages/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { pagesSlug } from '../../slugs.js'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: pagesSlug,
|
||||
admin: {
|
||||
useAsTitle: 'text',
|
||||
},
|
||||
enableQueryPresets: true,
|
||||
lockDocuments: false,
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
}
|
||||
21
test/query-presets/collections/Posts/index.ts
Normal file
21
test/query-presets/collections/Posts/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { postsSlug } from '../../slugs.js'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
slug: postsSlug,
|
||||
admin: {
|
||||
useAsTitle: 'text',
|
||||
},
|
||||
enableQueryPresets: true,
|
||||
lockDocuments: false,
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
}
|
||||
19
test/query-presets/collections/Users/index.ts
Normal file
19
test/query-presets/collections/Users/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { roles } from '../../fields/roles.js'
|
||||
import { usersSlug } from '../../slugs.js'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: usersSlug,
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
auth: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
roles,
|
||||
],
|
||||
}
|
||||
67
test/query-presets/config.ts
Normal file
67
test/query-presets/config.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { Pages } from './collections/Pages/index.js'
|
||||
import { Posts } from './collections/Posts/index.js'
|
||||
import { Users } from './collections/Users/index.js'
|
||||
import { roles } from './fields/roles.js'
|
||||
import { seed } from './seed.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
queryPresets: {
|
||||
// labels: {
|
||||
// singular: 'Report',
|
||||
// plural: 'Reports',
|
||||
// },
|
||||
access: {
|
||||
read: ({ req: { user } }) =>
|
||||
user ? !user?.roles?.some((role) => role === 'anonymous') : false,
|
||||
update: ({ req: { user } }) =>
|
||||
user ? !user?.roles?.some((role) => role === 'anonymous') : false,
|
||||
},
|
||||
constraints: {
|
||||
read: [
|
||||
{
|
||||
label: 'Specific Roles',
|
||||
value: 'specificRoles',
|
||||
fields: [roles],
|
||||
access: ({ req: { user } }) => ({
|
||||
'access.read.roles': {
|
||||
in: user?.roles || [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
update: [
|
||||
{
|
||||
label: 'Specific Roles',
|
||||
value: 'specificRoles',
|
||||
fields: [roles],
|
||||
access: ({ req: { user } }) => ({
|
||||
'access.update.roles': {
|
||||
in: user?.roles || [],
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
collections: [Pages, Users, Posts],
|
||||
onInit: async (payload) => {
|
||||
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
|
||||
await seed(payload)
|
||||
}
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
407
test/query-presets/e2e.spec.ts
Normal file
407
test/query-presets/e2e.spec.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
import type { BrowserContext, Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { devUser } from 'credentials.js'
|
||||
import { openListColumns } from 'helpers/e2e/openListColumns.js'
|
||||
import { toggleColumn } from 'helpers/e2e/toggleColumn.js'
|
||||
import * as path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
||||
import type { Config } from './payload-types.js'
|
||||
|
||||
import {
|
||||
ensureCompilationIsDone,
|
||||
exactText,
|
||||
initPageConsoleErrorCatch,
|
||||
saveDocAndAssert,
|
||||
// throttleTest,
|
||||
} from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { clickListMenuItem, openListMenu } from '../helpers/e2e/toggleListMenu.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
import { assertURLParams } from './helpers/assertURLParams.js'
|
||||
import { openQueryPresetDrawer } from './helpers/openQueryPresetDrawer.js'
|
||||
import { clearSelectedPreset, selectPreset } from './helpers/togglePreset.js'
|
||||
import { seedData } from './seed.js'
|
||||
import { pagesSlug } from './slugs.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const { beforeAll, describe, beforeEach } = test
|
||||
|
||||
let page: Page
|
||||
let pagesUrl: AdminUrlUtil
|
||||
let payload: PayloadTestSDK<Config>
|
||||
let serverURL: string
|
||||
let everyoneID: string | undefined
|
||||
let context: BrowserContext
|
||||
let user: any
|
||||
|
||||
describe('Query Presets', () => {
|
||||
beforeAll(async ({ browser }, testInfo) => {
|
||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({ dirname }))
|
||||
|
||||
pagesUrl = new AdminUrlUtil(serverURL, pagesSlug)
|
||||
|
||||
context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
|
||||
user = await payload
|
||||
.login({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
?.then((res) => res.user) // TODO: this type is wrong
|
||||
|
||||
initPageConsoleErrorCatch(page)
|
||||
|
||||
await ensureCompilationIsDone({ page, serverURL })
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
// await throttleTest({
|
||||
// page,
|
||||
// context,
|
||||
// delay: 'Fast 4G',
|
||||
// })
|
||||
|
||||
// clear and reseed everything
|
||||
try {
|
||||
await payload.delete({
|
||||
collection: 'payload-query-presets',
|
||||
where: {
|
||||
id: {
|
||||
exists: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const [, everyone] = await Promise.all([
|
||||
payload.delete({
|
||||
collection: 'payload-preferences',
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
key: { equals: 'pages-list' },
|
||||
},
|
||||
{
|
||||
'user.relationTo': {
|
||||
equals: 'users',
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.value': {
|
||||
equals: user.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
payload.create({
|
||||
collection: 'payload-query-presets',
|
||||
data: seedData.everyone,
|
||||
}),
|
||||
payload.create({
|
||||
collection: 'payload-query-presets',
|
||||
data: seedData.onlyMe,
|
||||
}),
|
||||
payload.create({
|
||||
collection: 'payload-query-presets',
|
||||
data: seedData.specificUsers({ userID: user?.id || '' }),
|
||||
}),
|
||||
])
|
||||
|
||||
everyoneID = everyone.id
|
||||
} catch (error) {
|
||||
console.error('Error in beforeEach:', error)
|
||||
}
|
||||
})
|
||||
|
||||
test('should select preset and apply filters', async () => {
|
||||
await page.goto(pagesUrl.list)
|
||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
||||
|
||||
await assertURLParams({
|
||||
page,
|
||||
columns: seedData.everyone.columns,
|
||||
where: seedData.everyone.where,
|
||||
presetID: everyoneID,
|
||||
})
|
||||
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
|
||||
test('should clear selected preset and reset filters', async () => {
|
||||
await page.goto(pagesUrl.list)
|
||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
||||
await clearSelectedPreset({ page })
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
|
||||
test('should delete a preset, clear selection, and reset changes', async () => {
|
||||
await page.goto(pagesUrl.list)
|
||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
||||
await openListMenu({ page })
|
||||
|
||||
await clickListMenuItem({ page, menuItemLabel: 'Delete' })
|
||||
|
||||
await page.locator('#confirm-delete-preset #confirm-action').click()
|
||||
|
||||
const regex = /columns=/
|
||||
|
||||
await page.waitForURL((url) => !regex.test(url.search), {
|
||||
timeout: TEST_TIMEOUT_LONG,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.locator('button#select-preset', {
|
||||
hasText: exactText('Select Preset'),
|
||||
}),
|
||||
).toBeVisible()
|
||||
|
||||
await openQueryPresetDrawer({ page })
|
||||
const modal = page.locator('[id^=list-drawer_0_]')
|
||||
await expect(modal).toBeVisible()
|
||||
|
||||
await expect(
|
||||
modal.locator('tbody tr td button', {
|
||||
hasText: exactText(seedData.everyone.title),
|
||||
}),
|
||||
).toBeHidden()
|
||||
})
|
||||
|
||||
test('should save last used preset to preferences and load on initial render', async () => {
|
||||
await page.goto(pagesUrl.list)
|
||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
||||
|
||||
await page.reload()
|
||||
|
||||
await assertURLParams({
|
||||
page,
|
||||
columns: seedData.everyone.columns,
|
||||
where: seedData.everyone.where,
|
||||
// presetID: everyoneID,
|
||||
})
|
||||
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
|
||||
test('should only show "edit" and "delete" controls when there is an active preset', async () => {
|
||||
await page.goto(pagesUrl.list)
|
||||
await openListMenu({ page })
|
||||
|
||||
await expect(
|
||||
page.locator('#list-menu .popup__content .popup-button-list__button', {
|
||||
hasText: exactText('Edit'),
|
||||
}),
|
||||
).toBeHidden()
|
||||
|
||||
await expect(
|
||||
page.locator('#list-menu .popup__content .popup-button-list__button', {
|
||||
hasText: exactText('Delete'),
|
||||
}),
|
||||
).toBeHidden()
|
||||
|
||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
||||
|
||||
await openListMenu({ page })
|
||||
|
||||
await expect(
|
||||
page.locator('#list-menu .popup__content .popup-button-list__button', {
|
||||
hasText: exactText('Edit'),
|
||||
}),
|
||||
).toBeVisible()
|
||||
|
||||
await expect(
|
||||
page.locator('#list-menu .popup__content .popup-button-list__button', {
|
||||
hasText: exactText('Delete'),
|
||||
}),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('should only show "reset" and "save" controls when there is an active preset and changes have been made', async () => {
|
||||
await page.goto(pagesUrl.list)
|
||||
|
||||
await openListMenu({ page })
|
||||
|
||||
await expect(
|
||||
page.locator('#list-menu .popup__content .popup-button-list__button', {
|
||||
hasText: exactText('Reset'),
|
||||
}),
|
||||
).toBeHidden()
|
||||
|
||||
await expect(
|
||||
page.locator('#list-menu .popup__content .popup-button-list__button', {
|
||||
hasText: exactText('Update for everyone'),
|
||||
}),
|
||||
).toBeHidden()
|
||||
|
||||
await expect(
|
||||
page.locator('#list-menu .popup__content .popup-button-list__button', {
|
||||
hasText: exactText('Save'),
|
||||
}),
|
||||
).toBeHidden()
|
||||
|
||||
await selectPreset({ page, presetTitle: seedData.onlyMe.title })
|
||||
|
||||
await toggleColumn(page, { columnLabel: 'ID' })
|
||||
|
||||
await openListMenu({ page })
|
||||
|
||||
await expect(
|
||||
page.locator('#list-menu .popup__content .popup-button-list__button', {
|
||||
hasText: exactText('Reset'),
|
||||
}),
|
||||
).toBeVisible()
|
||||
|
||||
await expect(
|
||||
page.locator('#list-menu .popup__content .popup-button-list__button', {
|
||||
hasText: exactText('Save'),
|
||||
}),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('should conditionally render "update for everyone" label based on if preset is shared', async () => {
|
||||
await page.goto(pagesUrl.list)
|
||||
|
||||
await selectPreset({ page, presetTitle: seedData.onlyMe.title })
|
||||
|
||||
await toggleColumn(page, { columnLabel: 'ID' })
|
||||
|
||||
await openListMenu({ page })
|
||||
|
||||
// When not shared, the label is "Save"
|
||||
await expect(
|
||||
page.locator('#list-menu .popup__content .popup-button-list__button', {
|
||||
hasText: exactText('Save'),
|
||||
}),
|
||||
).toBeVisible()
|
||||
|
||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
||||
|
||||
await toggleColumn(page, { columnLabel: 'ID' })
|
||||
|
||||
await openListMenu({ page })
|
||||
|
||||
// When shared, the label is "Update for everyone"
|
||||
await expect(
|
||||
page.locator('#list-menu .popup__content .popup-button-list__button', {
|
||||
hasText: exactText('Update for everyone'),
|
||||
}),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('should reset active changes', async () => {
|
||||
await page.goto(pagesUrl.list)
|
||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
||||
|
||||
const { columnContainer } = await toggleColumn(page, { columnLabel: 'ID' })
|
||||
|
||||
const column = columnContainer.locator(`.column-selector .column-selector__column`, {
|
||||
hasText: exactText('ID'),
|
||||
})
|
||||
|
||||
await openListMenu({ page })
|
||||
await clickListMenuItem({ page, menuItemLabel: 'Reset' })
|
||||
|
||||
await openListColumns(page, {})
|
||||
await expect(column).toHaveClass(/column-selector__column--active/)
|
||||
})
|
||||
|
||||
test('should only enter modified state when changes are made to an active preset', async () => {
|
||||
await page.goto(pagesUrl.list)
|
||||
await expect(page.locator('.list-controls__modified')).toBeHidden()
|
||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
||||
await expect(page.locator('.list-controls__modified')).toBeHidden()
|
||||
await toggleColumn(page, { columnLabel: 'ID' })
|
||||
await expect(page.locator('.list-controls__modified')).toBeVisible()
|
||||
await openListMenu({ page })
|
||||
await clickListMenuItem({ page, menuItemLabel: 'Update for everyone' })
|
||||
await expect(page.locator('.list-controls__modified')).toBeHidden()
|
||||
await toggleColumn(page, { columnLabel: 'ID' })
|
||||
await expect(page.locator('.list-controls__modified')).toBeVisible()
|
||||
await openListMenu({ page })
|
||||
await clickListMenuItem({ page, menuItemLabel: 'Reset' })
|
||||
await expect(page.locator('.list-controls__modified')).toBeHidden()
|
||||
})
|
||||
|
||||
test('can edit a preset through the document drawer', async () => {
|
||||
const presetTitle = 'New Preset'
|
||||
|
||||
await page.goto(pagesUrl.list)
|
||||
|
||||
await selectPreset({ page, presetTitle: seedData.everyone.title })
|
||||
await clickListMenuItem({ page, menuItemLabel: 'Edit' })
|
||||
|
||||
const drawer = page.locator('[id^=doc-drawer_payload-query-presets_0_]')
|
||||
const titleValue = drawer.locator('input[name="title"]')
|
||||
await expect(titleValue).toHaveValue(seedData.everyone.title)
|
||||
|
||||
const newTitle = `${seedData.everyone.title} (Updated)`
|
||||
await drawer.locator('input[name="title"]').fill(newTitle)
|
||||
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
await drawer.locator('button.doc-drawer__header-close').click()
|
||||
await expect(drawer).toBeHidden()
|
||||
|
||||
await expect(page.locator('button#select-preset')).toHaveText(newTitle)
|
||||
})
|
||||
|
||||
test('should not display query presets when admin.enableQueryPresets is not true', async () => {
|
||||
// go to users list view and ensure the query presets select is not visible
|
||||
const usersURL = new AdminUrlUtil(serverURL, 'users')
|
||||
await page.goto(usersURL.list)
|
||||
await expect(page.locator('#select-preset')).toBeHidden()
|
||||
})
|
||||
|
||||
// eslint-disable-next-line playwright/no-skipped-test, playwright/expect-expect
|
||||
test.skip('can save a preset', () => {
|
||||
// select a preset, make a change to the presets, click "save for everyone" or "save", and ensure the changes persist
|
||||
})
|
||||
|
||||
test('can create new preset', async () => {
|
||||
await page.goto(pagesUrl.list)
|
||||
|
||||
const presetTitle = 'New Preset'
|
||||
|
||||
await clickListMenuItem({ page, menuItemLabel: 'Create New' })
|
||||
const modal = page.locator('[id^=doc-drawer_payload-query-presets_0_]')
|
||||
await expect(modal).toBeVisible()
|
||||
await modal.locator('input[name="title"]').fill(presetTitle)
|
||||
|
||||
const currentURL = page.url()
|
||||
await saveDocAndAssert(page)
|
||||
await expect(modal).toBeHidden()
|
||||
|
||||
await page.waitForURL(() => page.url() !== currentURL)
|
||||
|
||||
await expect(
|
||||
page.locator('button#select-preset', {
|
||||
hasText: exactText(presetTitle),
|
||||
}),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('only shows query presets related to the underlying collection', async () => {
|
||||
// no results on `users` collection
|
||||
const postsUrl = new AdminUrlUtil(serverURL, 'posts')
|
||||
await page.goto(postsUrl.list)
|
||||
const drawer = await openQueryPresetDrawer({ page })
|
||||
await expect(drawer.locator('.table table > tbody > tr')).toHaveCount(0)
|
||||
await expect(drawer.locator('.collection-list__no-results')).toBeVisible()
|
||||
|
||||
// results on `pages` collection
|
||||
await page.goto(pagesUrl.list)
|
||||
await openQueryPresetDrawer({ page })
|
||||
await expect(drawer.locator('.table table > tbody > tr')).toHaveCount(3)
|
||||
await drawer.locator('.collection-list__no-results').isHidden()
|
||||
})
|
||||
})
|
||||
23
test/query-presets/eslint.config.js
Normal file
23
test/query-presets/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { rootParserOptions } from '../../eslint.config.js'
|
||||
import { testEslintConfig } from '../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.Config} Config */
|
||||
|
||||
/** @type {Config[]} */
|
||||
export const index = [
|
||||
...testEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: {
|
||||
allowDefaultProject: ['./*.ts', './*.tsx'],
|
||||
defaultProject: './tsconfig.json',
|
||||
},
|
||||
tsconfigDirName: import.meta.dirname,
|
||||
...rootParserOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
21
test/query-presets/fields/roles.ts
Normal file
21
test/query-presets/fields/roles.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
export const roles: Field = {
|
||||
name: 'roles',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
options: [
|
||||
{
|
||||
label: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
{
|
||||
label: 'User',
|
||||
value: 'user',
|
||||
},
|
||||
{
|
||||
label: 'Anonymous',
|
||||
value: 'anonymous',
|
||||
},
|
||||
],
|
||||
}
|
||||
39
test/query-presets/helpers/assertURLParams.ts
Normal file
39
test/query-presets/helpers/assertURLParams.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import type { ColumnPreference, Where } from 'payload'
|
||||
|
||||
// import { transformColumnsToSearchParams, transformWhereQuery } from 'payload/shared'
|
||||
// import * as qs from 'qs-esm'
|
||||
|
||||
import { transformColumnsToSearchParams } from 'payload/shared'
|
||||
|
||||
export async function assertURLParams({
|
||||
page,
|
||||
columns,
|
||||
where,
|
||||
presetID,
|
||||
}: {
|
||||
columns?: ColumnPreference[]
|
||||
page: Page
|
||||
presetID?: string | undefined
|
||||
where: Where
|
||||
}) {
|
||||
if (where) {
|
||||
// TODO: can't get columns to encode correctly
|
||||
// const whereQuery = qs.stringify(transformWhereQuery(where))
|
||||
// const encodedWhere = encodeURIComponent(whereQuery)
|
||||
}
|
||||
|
||||
if (columns) {
|
||||
const escapedColumns = encodeURIComponent(
|
||||
JSON.stringify(transformColumnsToSearchParams(columns)),
|
||||
)
|
||||
|
||||
const columnsRegex = new RegExp(`columns=${escapedColumns}`)
|
||||
await page.waitForURL(columnsRegex)
|
||||
}
|
||||
|
||||
if (presetID) {
|
||||
const presetRegex = new RegExp(`preset=${presetID}`)
|
||||
await page.waitForURL(presetRegex)
|
||||
}
|
||||
}
|
||||
11
test/query-presets/helpers/openQueryPresetDrawer.ts
Normal file
11
test/query-presets/helpers/openQueryPresetDrawer.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
export async function openQueryPresetDrawer({ page }: { page: Page }): Promise<Locator> {
|
||||
await page.click('button#select-preset')
|
||||
const drawer = page.locator('dialog[id^="list-drawer_0_"]')
|
||||
await expect(drawer).toBeVisible()
|
||||
await expect(drawer.locator('.collection-list--payload-query-presets')).toBeVisible()
|
||||
return drawer
|
||||
}
|
||||
53
test/query-presets/helpers/togglePreset.ts
Normal file
53
test/query-presets/helpers/togglePreset.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import { exactText } from 'helpers.js'
|
||||
import { TEST_TIMEOUT_LONG } from 'playwright.config.js'
|
||||
|
||||
import { openQueryPresetDrawer } from './openQueryPresetDrawer.js'
|
||||
|
||||
export async function selectPreset({ page, presetTitle }: { page: Page; presetTitle: string }) {
|
||||
await openQueryPresetDrawer({ page })
|
||||
const modal = page.locator('[id^=list-drawer_0_]')
|
||||
await expect(modal).toBeVisible()
|
||||
|
||||
const currentURL = page.url()
|
||||
|
||||
await modal
|
||||
.locator('tbody tr td button', {
|
||||
hasText: exactText(presetTitle),
|
||||
})
|
||||
.first()
|
||||
.click()
|
||||
|
||||
await page.waitForURL(() => page.url() !== currentURL)
|
||||
|
||||
await expect(
|
||||
page.locator('button#select-preset', {
|
||||
hasText: exactText(presetTitle),
|
||||
}),
|
||||
).toBeVisible()
|
||||
}
|
||||
|
||||
export async function clearSelectedPreset({ page }: { page: Page }) {
|
||||
const queryPresetsControl = page.locator('button#select-preset')
|
||||
const clearButton = queryPresetsControl.locator('#clear-preset')
|
||||
|
||||
if (await clearButton.isVisible()) {
|
||||
await clearButton.click()
|
||||
}
|
||||
|
||||
const regex = /columns=/
|
||||
|
||||
await page.waitForURL((url) => !regex.test(url.search), {
|
||||
timeout: TEST_TIMEOUT_LONG,
|
||||
})
|
||||
|
||||
await expect(queryPresetsControl.locator('#clear-preset')).toBeHidden()
|
||||
|
||||
await expect(
|
||||
page.locator('button#select-preset', {
|
||||
hasText: exactText('Select Preset'),
|
||||
}),
|
||||
).toBeVisible()
|
||||
}
|
||||
568
test/query-presets/int.spec.ts
Normal file
568
test/query-presets/int.spec.ts
Normal file
@@ -0,0 +1,568 @@
|
||||
import type { NextRESTClient } from 'helpers/NextRESTClient.js'
|
||||
import type { Payload, User } from 'payload'
|
||||
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { devUser, regularUser } from '../credentials.js'
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
|
||||
const queryPresetsCollectionSlug = 'payload-query-presets'
|
||||
|
||||
let payload: Payload
|
||||
let restClient: NextRESTClient
|
||||
let user: User
|
||||
let user2: User
|
||||
let anonymousUser: User
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
describe('Query Presets', () => {
|
||||
beforeAll(async () => {
|
||||
// @ts-expect-error: initPayloadInt does not have a proper type definition
|
||||
;({ payload, restClient } = await initPayloadInt(dirname))
|
||||
|
||||
user = await payload
|
||||
.login({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
?.then((result) => result.user)
|
||||
|
||||
user2 = await payload
|
||||
.login({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: regularUser.email,
|
||||
password: regularUser.password,
|
||||
},
|
||||
})
|
||||
?.then((result) => result.user)
|
||||
|
||||
anonymousUser = await payload
|
||||
.login({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'anonymous@email.com',
|
||||
password: regularUser.password,
|
||||
},
|
||||
})
|
||||
?.then((result) => result.user)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (typeof payload.db.destroy === 'function') {
|
||||
await payload.db.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
describe('default access control', () => {
|
||||
it('should only allow logged in users to perform actions', async () => {
|
||||
// create
|
||||
try {
|
||||
const result = await payload.create({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
user: undefined,
|
||||
overrideAccess: false,
|
||||
data: {
|
||||
title: 'Only Logged In Users',
|
||||
relatedCollection: 'pages',
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toBeFalsy()
|
||||
} catch (error: unknown) {
|
||||
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
||||
}
|
||||
|
||||
const { id } = await payload.create({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
data: {
|
||||
title: 'Only Logged In Users',
|
||||
relatedCollection: 'pages',
|
||||
},
|
||||
})
|
||||
|
||||
// read
|
||||
try {
|
||||
const result = await payload.findByID({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
depth: 0,
|
||||
user: undefined,
|
||||
overrideAccess: false,
|
||||
id,
|
||||
})
|
||||
|
||||
expect(result).toBeFalsy()
|
||||
} catch (error: unknown) {
|
||||
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
||||
}
|
||||
|
||||
// update
|
||||
try {
|
||||
const result = await payload.update({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
id,
|
||||
user: undefined,
|
||||
overrideAccess: false,
|
||||
data: {
|
||||
title: 'Only Logged In Users (Updated)',
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toBeFalsy()
|
||||
} catch (error: unknown) {
|
||||
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
||||
|
||||
// make sure the update didn't go through
|
||||
const preset = await payload.findByID({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
depth: 0,
|
||||
id,
|
||||
})
|
||||
|
||||
expect(preset.title).toBe('Only Logged In Users')
|
||||
}
|
||||
|
||||
// delete
|
||||
try {
|
||||
const result = await payload.delete({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
id: 'some-id',
|
||||
user: undefined,
|
||||
overrideAccess: false,
|
||||
})
|
||||
|
||||
expect(result).toBeFalsy()
|
||||
} catch (error: unknown) {
|
||||
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
||||
|
||||
// make sure the delete didn't go through
|
||||
const preset = await payload.findByID({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
depth: 0,
|
||||
id,
|
||||
})
|
||||
|
||||
expect(preset.title).toBe('Only Logged In Users')
|
||||
}
|
||||
})
|
||||
|
||||
it('should respect access when set to "specificUsers"', async () => {
|
||||
const presetForSpecificUsers = await payload.create({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
user,
|
||||
data: {
|
||||
title: 'Specific Users',
|
||||
where: {
|
||||
text: {
|
||||
equals: 'example page',
|
||||
},
|
||||
},
|
||||
access: {
|
||||
read: {
|
||||
constraint: 'specificUsers',
|
||||
users: [user.id],
|
||||
},
|
||||
update: {
|
||||
constraint: 'specificUsers',
|
||||
users: [user.id],
|
||||
},
|
||||
},
|
||||
relatedCollection: 'pages',
|
||||
},
|
||||
})
|
||||
|
||||
const foundPresetWithUser1 = await payload.findByID({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
depth: 0,
|
||||
user,
|
||||
overrideAccess: false,
|
||||
id: presetForSpecificUsers.id,
|
||||
})
|
||||
|
||||
expect(foundPresetWithUser1.id).toBe(presetForSpecificUsers.id)
|
||||
|
||||
try {
|
||||
const foundPresetWithUser2 = await payload.findByID({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
depth: 0,
|
||||
user: user2,
|
||||
overrideAccess: false,
|
||||
id: presetForSpecificUsers.id,
|
||||
})
|
||||
|
||||
expect(foundPresetWithUser2).toBeFalsy()
|
||||
} catch (error: unknown) {
|
||||
expect((error as Error).message).toBe('Not Found')
|
||||
}
|
||||
|
||||
const presetUpdatedByUser1 = await payload.update({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
id: presetForSpecificUsers.id,
|
||||
user,
|
||||
overrideAccess: false,
|
||||
data: {
|
||||
title: 'Specific Users (Updated)',
|
||||
},
|
||||
})
|
||||
|
||||
expect(presetUpdatedByUser1.title).toBe('Specific Users (Updated)')
|
||||
|
||||
try {
|
||||
const presetUpdatedByUser2 = await payload.update({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
id: presetForSpecificUsers.id,
|
||||
user: user2,
|
||||
overrideAccess: false,
|
||||
data: {
|
||||
title: 'Specific Users (Updated)',
|
||||
},
|
||||
})
|
||||
|
||||
expect(presetUpdatedByUser2).toBeFalsy()
|
||||
} catch (error: unknown) {
|
||||
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should respect access when set to "onlyMe"', async () => {
|
||||
// create a new doc so that the creating user is the owner
|
||||
const presetForOnlyMe = await payload.create({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
user,
|
||||
data: {
|
||||
title: 'Only Me',
|
||||
where: {
|
||||
text: {
|
||||
equals: 'example page',
|
||||
},
|
||||
},
|
||||
access: {
|
||||
read: {
|
||||
constraint: 'onlyMe',
|
||||
},
|
||||
update: {
|
||||
constraint: 'onlyMe',
|
||||
},
|
||||
},
|
||||
relatedCollection: 'pages',
|
||||
},
|
||||
})
|
||||
|
||||
const foundPresetWithUser1 = await payload.findByID({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
depth: 0,
|
||||
user,
|
||||
overrideAccess: false,
|
||||
id: presetForOnlyMe.id,
|
||||
})
|
||||
|
||||
expect(foundPresetWithUser1.id).toBe(presetForOnlyMe.id)
|
||||
|
||||
try {
|
||||
const foundPresetWithUser2 = await payload.findByID({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
depth: 0,
|
||||
user: user2,
|
||||
overrideAccess: false,
|
||||
id: presetForOnlyMe.id,
|
||||
})
|
||||
|
||||
expect(foundPresetWithUser2).toBeFalsy()
|
||||
} catch (error: unknown) {
|
||||
expect((error as Error).message).toBe('Not Found')
|
||||
}
|
||||
|
||||
const presetUpdatedByUser1 = await payload.update({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
id: presetForOnlyMe.id,
|
||||
user,
|
||||
overrideAccess: false,
|
||||
data: {
|
||||
title: 'Only Me (Updated)',
|
||||
},
|
||||
})
|
||||
|
||||
expect(presetUpdatedByUser1.title).toBe('Only Me (Updated)')
|
||||
|
||||
try {
|
||||
const presetUpdatedByUser2 = await payload.update({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
id: presetForOnlyMe.id,
|
||||
user: user2,
|
||||
overrideAccess: false,
|
||||
data: {
|
||||
title: 'Only Me (Updated)',
|
||||
},
|
||||
})
|
||||
|
||||
expect(presetUpdatedByUser2).toBeFalsy()
|
||||
} catch (error: unknown) {
|
||||
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should respect access when set to "everyone"', async () => {
|
||||
const presetForEveryone = await payload.create({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
user,
|
||||
data: {
|
||||
title: 'Everyone',
|
||||
where: {
|
||||
text: {
|
||||
equals: 'example page',
|
||||
},
|
||||
},
|
||||
access: {
|
||||
read: {
|
||||
constraint: 'everyone',
|
||||
},
|
||||
update: {
|
||||
constraint: 'everyone',
|
||||
},
|
||||
delete: {
|
||||
constraint: 'everyone',
|
||||
},
|
||||
},
|
||||
relatedCollection: 'pages',
|
||||
},
|
||||
})
|
||||
|
||||
const foundPresetWithUser1 = await payload.findByID({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
depth: 0,
|
||||
user,
|
||||
overrideAccess: false,
|
||||
id: presetForEveryone.id,
|
||||
})
|
||||
|
||||
expect(foundPresetWithUser1.id).toBe(presetForEveryone.id)
|
||||
|
||||
const foundPresetWithUser2 = await payload.findByID({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
depth: 0,
|
||||
user: user2,
|
||||
overrideAccess: false,
|
||||
id: presetForEveryone.id,
|
||||
})
|
||||
|
||||
expect(foundPresetWithUser2.id).toBe(presetForEveryone.id)
|
||||
|
||||
const presetUpdatedByUser1 = await payload.update({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
id: presetForEveryone.id,
|
||||
user,
|
||||
overrideAccess: false,
|
||||
data: {
|
||||
title: 'Everyone (Update 1)',
|
||||
},
|
||||
})
|
||||
|
||||
expect(presetUpdatedByUser1.title).toBe('Everyone (Update 1)')
|
||||
|
||||
const presetUpdatedByUser2 = await payload.update({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
id: presetForEveryone.id,
|
||||
user: user2,
|
||||
overrideAccess: false,
|
||||
data: {
|
||||
title: 'Everyone (Update 2)',
|
||||
},
|
||||
})
|
||||
|
||||
expect(presetUpdatedByUser2.title).toBe('Everyone (Update 2)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('user-defined access control', () => {
|
||||
it('should respect top-level access control overrides', async () => {
|
||||
const preset = await payload.create({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
user,
|
||||
data: {
|
||||
title: 'Top-Level Access Control Override',
|
||||
relatedCollection: 'pages',
|
||||
access: {
|
||||
read: {
|
||||
constraint: 'everyone',
|
||||
},
|
||||
update: {
|
||||
constraint: 'everyone',
|
||||
},
|
||||
delete: {
|
||||
constraint: 'everyone',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const foundPresetWithUser1 = await payload.findByID({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
depth: 0,
|
||||
user,
|
||||
overrideAccess: false,
|
||||
id: preset.id,
|
||||
})
|
||||
|
||||
expect(foundPresetWithUser1.id).toBe(preset.id)
|
||||
|
||||
try {
|
||||
const foundPresetWithAnonymousUser = await payload.findByID({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
depth: 0,
|
||||
user: anonymousUser,
|
||||
overrideAccess: false,
|
||||
id: preset.id,
|
||||
})
|
||||
|
||||
expect(foundPresetWithAnonymousUser).toBeFalsy()
|
||||
} catch (error: unknown) {
|
||||
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
||||
}
|
||||
})
|
||||
|
||||
it('should respect access when set to "specificRoles"', async () => {
|
||||
const presetForSpecificRoles = await payload.create({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
user,
|
||||
data: {
|
||||
title: 'Specific Roles',
|
||||
where: {
|
||||
text: {
|
||||
equals: 'example page',
|
||||
},
|
||||
},
|
||||
access: {
|
||||
read: {
|
||||
constraint: 'specificRoles',
|
||||
roles: ['admin'],
|
||||
},
|
||||
update: {
|
||||
constraint: 'specificRoles',
|
||||
roles: ['admin'],
|
||||
},
|
||||
},
|
||||
relatedCollection: 'pages',
|
||||
},
|
||||
})
|
||||
|
||||
const foundPresetWithUser1 = await payload.findByID({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
depth: 0,
|
||||
user,
|
||||
overrideAccess: false,
|
||||
id: presetForSpecificRoles.id,
|
||||
})
|
||||
|
||||
expect(foundPresetWithUser1.id).toBe(presetForSpecificRoles.id)
|
||||
|
||||
try {
|
||||
const foundPresetWithUser2 = await payload.findByID({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
depth: 0,
|
||||
user: user2,
|
||||
overrideAccess: false,
|
||||
id: presetForSpecificRoles.id,
|
||||
})
|
||||
|
||||
expect(foundPresetWithUser2).toBeFalsy()
|
||||
} catch (error: unknown) {
|
||||
expect((error as Error).message).toBe('Not Found')
|
||||
}
|
||||
|
||||
const presetUpdatedByUser1 = await payload.update({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
id: presetForSpecificRoles.id,
|
||||
user,
|
||||
overrideAccess: false,
|
||||
data: {
|
||||
title: 'Specific Roles (Updated)',
|
||||
},
|
||||
})
|
||||
|
||||
expect(presetUpdatedByUser1.title).toBe('Specific Roles (Updated)')
|
||||
|
||||
try {
|
||||
const presetUpdatedByUser2 = await payload.update({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
id: presetForSpecificRoles.id,
|
||||
user: user2,
|
||||
overrideAccess: false,
|
||||
data: {
|
||||
title: 'Specific Roles (Updated)',
|
||||
},
|
||||
})
|
||||
|
||||
expect(presetUpdatedByUser2).toBeFalsy()
|
||||
} catch (error: unknown) {
|
||||
expect((error as Error).message).toBe('You are not allowed to perform this action.')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('should disable query presets when "enabledQueryPresets" is not true on the collection', async () => {
|
||||
try {
|
||||
const result = await payload.create({
|
||||
collection: 'payload-query-presets',
|
||||
user,
|
||||
data: {
|
||||
title: 'Disabled Query Presets',
|
||||
relatedCollection: 'pages',
|
||||
},
|
||||
})
|
||||
|
||||
// TODO: this test always passes because this expect throws an error which is caught and passes the 'catch' block
|
||||
expect(result).toBeFalsy()
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
describe('Where object formatting', () => {
|
||||
it('transforms "where" query objects into the "and" / "or" format', async () => {
|
||||
const result = await payload.create({
|
||||
collection: queryPresetsCollectionSlug,
|
||||
user,
|
||||
data: {
|
||||
title: 'Where Object Formatting',
|
||||
where: {
|
||||
text: {
|
||||
equals: 'example page',
|
||||
},
|
||||
},
|
||||
access: {
|
||||
read: {
|
||||
constraint: 'everyone',
|
||||
},
|
||||
update: {
|
||||
constraint: 'everyone',
|
||||
},
|
||||
delete: {
|
||||
constraint: 'everyone',
|
||||
},
|
||||
},
|
||||
relatedCollection: 'pages',
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.where).toMatchObject({
|
||||
or: [
|
||||
{
|
||||
and: [
|
||||
{
|
||||
text: {
|
||||
equals: 'example page',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
386
test/query-presets/payload-types.ts
Normal file
386
test/query-presets/payload-types.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
/* 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: {
|
||||
pages: Page;
|
||||
users: User;
|
||||
posts: Post;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
'payload-query-presets': PayloadQueryPreset;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
pages: PagesSelect<false> | PagesSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
'payload-query-presets': PayloadQueryPresetsSelect<false> | PayloadQueryPresetsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: number;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: null;
|
||||
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` "pages".
|
||||
*/
|
||||
export interface Page {
|
||||
id: number;
|
||||
text?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
roles?: ('admin' | 'user' | 'anonymous')[] | null;
|
||||
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` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: number;
|
||||
text?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: number;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: number | Page;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: number | Post;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: number | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: number;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: number | 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: number;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-query-presets".
|
||||
*/
|
||||
export interface PayloadQueryPreset {
|
||||
id: number;
|
||||
title: string;
|
||||
isShared?: boolean | null;
|
||||
access?: {
|
||||
read?: {
|
||||
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles') | null;
|
||||
users?: (number | User)[] | null;
|
||||
roles?: ('admin' | 'user' | 'anonymous')[] | null;
|
||||
};
|
||||
update?: {
|
||||
constraint?: ('everyone' | 'onlyMe' | 'specificUsers' | 'specificRoles') | null;
|
||||
users?: (number | User)[] | null;
|
||||
roles?: ('admin' | 'user' | 'anonymous')[] | null;
|
||||
};
|
||||
delete?: {
|
||||
constraint?: ('everyone' | 'onlyMe' | 'specificUsers') | null;
|
||||
users?: (number | User)[] | null;
|
||||
};
|
||||
};
|
||||
where?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
columns?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
relatedCollection: 'pages' | 'posts';
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages_select".
|
||||
*/
|
||||
export interface PagesSelect<T extends boolean = true> {
|
||||
text?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
name?: 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` "posts_select".
|
||||
*/
|
||||
export interface PostsSelect<T extends boolean = true> {
|
||||
text?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: 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` "payload-query-presets_select".
|
||||
*/
|
||||
export interface PayloadQueryPresetsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
isShared?: T;
|
||||
access?:
|
||||
| T
|
||||
| {
|
||||
read?:
|
||||
| T
|
||||
| {
|
||||
constraint?: T;
|
||||
users?: T;
|
||||
roles?: T;
|
||||
};
|
||||
update?:
|
||||
| T
|
||||
| {
|
||||
constraint?: T;
|
||||
users?: T;
|
||||
roles?: T;
|
||||
};
|
||||
delete?:
|
||||
| T
|
||||
| {
|
||||
constraint?: T;
|
||||
users?: T;
|
||||
};
|
||||
};
|
||||
where?: T;
|
||||
columns?: T;
|
||||
relatedCollection?: 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 {}
|
||||
}
|
||||
2693
test/query-presets/schema.graphql
Normal file
2693
test/query-presets/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
182
test/query-presets/seed.ts
Normal file
182
test/query-presets/seed.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import type { Payload, QueryPreset } from 'payload'
|
||||
|
||||
import { devUser as devCredentials, regularUser as regularCredentials } from '../credentials.js'
|
||||
import { executePromises } from '../helpers/executePromises.js'
|
||||
import { seedDB } from '../helpers/seed.js'
|
||||
import { collectionSlugs, pagesSlug, usersSlug } from './slugs.js'
|
||||
|
||||
type SeededQueryPreset = {
|
||||
relatedCollection: 'pages'
|
||||
} & Omit<QueryPreset, 'id' | 'relatedCollection'>
|
||||
|
||||
export const seedData: {
|
||||
everyone: SeededQueryPreset
|
||||
onlyMe: SeededQueryPreset
|
||||
specificUsers: (args: { userID: string }) => SeededQueryPreset
|
||||
} = {
|
||||
onlyMe: {
|
||||
relatedCollection: pagesSlug,
|
||||
isShared: false,
|
||||
title: 'Only Me',
|
||||
columns: [
|
||||
{
|
||||
accessor: 'text',
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
access: {
|
||||
delete: {
|
||||
constraint: 'onlyMe',
|
||||
},
|
||||
update: {
|
||||
constraint: 'onlyMe',
|
||||
},
|
||||
read: {
|
||||
constraint: 'onlyMe',
|
||||
},
|
||||
},
|
||||
where: {
|
||||
text: {
|
||||
equals: 'example page',
|
||||
},
|
||||
},
|
||||
},
|
||||
everyone: {
|
||||
relatedCollection: pagesSlug,
|
||||
isShared: true,
|
||||
title: 'Everyone',
|
||||
access: {
|
||||
delete: {
|
||||
constraint: 'everyone',
|
||||
},
|
||||
update: {
|
||||
constraint: 'everyone',
|
||||
},
|
||||
read: {
|
||||
constraint: 'everyone',
|
||||
},
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
accessor: 'text',
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
where: {
|
||||
text: {
|
||||
equals: 'example page',
|
||||
},
|
||||
},
|
||||
},
|
||||
specificUsers: ({ userID }: { userID: string }) => ({
|
||||
title: 'Specific Users',
|
||||
isShared: true,
|
||||
where: {
|
||||
text: {
|
||||
equals: 'example page',
|
||||
},
|
||||
},
|
||||
access: {
|
||||
read: {
|
||||
constraint: 'specificUsers',
|
||||
users: [userID],
|
||||
},
|
||||
update: {
|
||||
constraint: 'specificUsers',
|
||||
users: [userID],
|
||||
},
|
||||
delete: {
|
||||
constraint: 'specificUsers',
|
||||
users: [userID],
|
||||
},
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
accessor: 'text',
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
relatedCollection: pagesSlug,
|
||||
}),
|
||||
}
|
||||
|
||||
export const seed = async (_payload: Payload) => {
|
||||
const [devUser] = await executePromises(
|
||||
[
|
||||
() =>
|
||||
_payload.create({
|
||||
collection: usersSlug,
|
||||
data: {
|
||||
email: devCredentials.email,
|
||||
password: devCredentials.password,
|
||||
name: 'Admin',
|
||||
roles: ['admin'],
|
||||
},
|
||||
}),
|
||||
() =>
|
||||
_payload.create({
|
||||
collection: usersSlug,
|
||||
data: {
|
||||
email: regularCredentials.email,
|
||||
password: regularCredentials.password,
|
||||
name: 'User',
|
||||
roles: ['user'],
|
||||
},
|
||||
}),
|
||||
() =>
|
||||
_payload.create({
|
||||
collection: usersSlug,
|
||||
data: {
|
||||
email: 'anonymous@email.com',
|
||||
password: regularCredentials.password,
|
||||
name: 'User',
|
||||
roles: ['anonymous'],
|
||||
},
|
||||
}),
|
||||
],
|
||||
false,
|
||||
)
|
||||
|
||||
await executePromises(
|
||||
[
|
||||
() =>
|
||||
_payload.create({
|
||||
collection: pagesSlug,
|
||||
data: {
|
||||
text: 'example page',
|
||||
},
|
||||
}),
|
||||
() =>
|
||||
_payload.create({
|
||||
collection: 'payload-query-presets',
|
||||
user: devUser,
|
||||
overrideAccess: false,
|
||||
data: seedData.specificUsers({ userID: devUser?.id || '' }),
|
||||
}),
|
||||
() =>
|
||||
_payload.create({
|
||||
collection: 'payload-query-presets',
|
||||
user: devUser,
|
||||
overrideAccess: false,
|
||||
data: seedData.everyone,
|
||||
}),
|
||||
() =>
|
||||
_payload.create({
|
||||
collection: 'payload-query-presets',
|
||||
user: devUser,
|
||||
overrideAccess: false,
|
||||
data: seedData.onlyMe,
|
||||
}),
|
||||
],
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
export async function clearAndSeedEverything(_payload: Payload) {
|
||||
return await seedDB({
|
||||
_payload,
|
||||
collectionSlugs,
|
||||
seedFunction: seed,
|
||||
snapshotKey: 'adminTests',
|
||||
})
|
||||
}
|
||||
7
test/query-presets/slugs.ts
Normal file
7
test/query-presets/slugs.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const usersSlug = 'users'
|
||||
|
||||
export const pagesSlug = 'pages'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
|
||||
export const collectionSlugs = [usersSlug, pagesSlug, postsSlug]
|
||||
13
test/query-presets/tsconfig.eslint.json
Normal file
13
test/query-presets/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// extend your base config to share compilerOptions, etc
|
||||
//"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// ensure that nobody can accidentally use this config for a build
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
// whatever paths you intend to lint
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx"
|
||||
]
|
||||
}
|
||||
3
test/query-presets/tsconfig.json
Normal file
3
test/query-presets/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../tsconfig.json"
|
||||
}
|
||||
@@ -10,8 +10,10 @@ import { updatePostStep1, updatePostStep2 } from './runners/updatePost.js'
|
||||
import { clearAndSeedEverything } from './seed.js'
|
||||
import { externalWorkflow } from './workflows/externalWorkflow.js'
|
||||
import { inlineTaskTestWorkflow } from './workflows/inlineTaskTest.js'
|
||||
import { inlineTaskTestDelayedWorkflow } from './workflows/inlineTaskTestDelayed.js'
|
||||
import { longRunningWorkflow } from './workflows/longRunning.js'
|
||||
import { noRetriesSetWorkflow } from './workflows/noRetriesSet.js'
|
||||
import { parallelTaskWorkflow } from './workflows/parallelTaskWorkflow.js'
|
||||
import { retries0Workflow } from './workflows/retries0.js'
|
||||
import { retriesBackoffTestWorkflow } from './workflows/retriesBackoffTest.js'
|
||||
import { retriesRollbackTestWorkflow } from './workflows/retriesRollbackTest.js'
|
||||
@@ -103,6 +105,11 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
}
|
||||
},
|
||||
processingOrder: {
|
||||
queues: {
|
||||
lifo: '-createdAt',
|
||||
},
|
||||
},
|
||||
tasks: [
|
||||
{
|
||||
retries: 2,
|
||||
@@ -375,11 +382,13 @@ export default buildConfigWithDefaults({
|
||||
workflowRetries2TasksRetriesUndefinedWorkflow,
|
||||
workflowRetries2TasksRetries0Workflow,
|
||||
inlineTaskTestWorkflow,
|
||||
inlineTaskTestDelayedWorkflow,
|
||||
externalWorkflow,
|
||||
retriesBackoffTestWorkflow,
|
||||
subTaskWorkflow,
|
||||
subTaskFailsWorkflow,
|
||||
longRunningWorkflow,
|
||||
parallelTaskWorkflow,
|
||||
],
|
||||
},
|
||||
editor: lexicalEditor(),
|
||||
|
||||
@@ -533,6 +533,106 @@ describe('Queues', () => {
|
||||
payload.config.jobs.deleteJobOnComplete = true
|
||||
})
|
||||
|
||||
it('ensure jobs run in FIFO order by default', async () => {
|
||||
await payload.jobs.queue({
|
||||
workflow: 'inlineTaskTestDelayed',
|
||||
input: {
|
||||
message: 'task 1',
|
||||
},
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
await payload.jobs.queue({
|
||||
workflow: 'inlineTaskTestDelayed',
|
||||
input: {
|
||||
message: 'task 2',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.jobs.run({
|
||||
sequential: true,
|
||||
})
|
||||
|
||||
const allSimples = await payload.find({
|
||||
collection: 'simple',
|
||||
limit: 100,
|
||||
sort: 'createdAt',
|
||||
})
|
||||
|
||||
expect(allSimples.totalDocs).toBe(2)
|
||||
expect(allSimples.docs?.[0]?.title).toBe('task 1')
|
||||
expect(allSimples.docs?.[1]?.title).toBe('task 2')
|
||||
})
|
||||
|
||||
it('ensure jobs can run LIFO if processingOrder is passed', async () => {
|
||||
await payload.jobs.queue({
|
||||
workflow: 'inlineTaskTestDelayed',
|
||||
input: {
|
||||
message: 'task 1',
|
||||
},
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
await payload.jobs.queue({
|
||||
workflow: 'inlineTaskTestDelayed',
|
||||
input: {
|
||||
message: 'task 2',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.jobs.run({
|
||||
sequential: true,
|
||||
processingOrder: '-createdAt',
|
||||
})
|
||||
|
||||
const allSimples = await payload.find({
|
||||
collection: 'simple',
|
||||
limit: 100,
|
||||
sort: 'createdAt',
|
||||
})
|
||||
|
||||
expect(allSimples.totalDocs).toBe(2)
|
||||
expect(allSimples.docs?.[0]?.title).toBe('task 2')
|
||||
expect(allSimples.docs?.[1]?.title).toBe('task 1')
|
||||
})
|
||||
|
||||
it('ensure job config processingOrder using queues object is respected', async () => {
|
||||
await payload.jobs.queue({
|
||||
workflow: 'inlineTaskTestDelayed',
|
||||
queue: 'lifo',
|
||||
input: {
|
||||
message: 'task 1',
|
||||
},
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
await payload.jobs.queue({
|
||||
workflow: 'inlineTaskTestDelayed',
|
||||
queue: 'lifo',
|
||||
input: {
|
||||
message: 'task 2',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.jobs.run({
|
||||
sequential: true,
|
||||
queue: 'lifo',
|
||||
})
|
||||
|
||||
const allSimples = await payload.find({
|
||||
collection: 'simple',
|
||||
limit: 100,
|
||||
sort: 'createdAt',
|
||||
})
|
||||
|
||||
expect(allSimples.totalDocs).toBe(2)
|
||||
expect(allSimples.docs?.[0]?.title).toBe('task 2')
|
||||
expect(allSimples.docs?.[1]?.title).toBe('task 1')
|
||||
})
|
||||
|
||||
it('can create new inline jobs', async () => {
|
||||
await payload.jobs.queue({
|
||||
workflow: 'inlineTaskTest',
|
||||
@@ -1265,4 +1365,40 @@ describe('Queues', () => {
|
||||
expect(jobAfterRun.log[0].error.message).toBe('custom error message')
|
||||
expect(jobAfterRun.log[0].state).toBe('failed')
|
||||
})
|
||||
|
||||
it('can reliably run workflows with parallel tasks', async () => {
|
||||
const amount = 500
|
||||
payload.config.jobs.deleteJobOnComplete = false
|
||||
|
||||
const job = await payload.jobs.queue({
|
||||
workflow: 'parallelTask',
|
||||
input: {},
|
||||
})
|
||||
|
||||
await payload.jobs.run()
|
||||
|
||||
const jobAfterRun = await payload.findByID({
|
||||
collection: 'payload-jobs',
|
||||
id: job.id,
|
||||
})
|
||||
|
||||
expect(jobAfterRun.hasError).toBe(false)
|
||||
expect(jobAfterRun.log?.length).toBe(amount)
|
||||
|
||||
const simpleDocs = await payload.find({
|
||||
collection: 'simple',
|
||||
limit: amount,
|
||||
depth: 0,
|
||||
})
|
||||
expect(simpleDocs.docs).toHaveLength(amount)
|
||||
|
||||
// Ensure all docs are created (= all tasks are run once)
|
||||
for (let i = 1; i <= simpleDocs.docs.length; i++) {
|
||||
const simpleDoc = simpleDocs.docs.find((doc) => doc.title === `parallel task ${i}`)
|
||||
const logEntry = jobAfterRun?.log?.find((log) => log.taskID === `parallel task ${i}`)
|
||||
expect(simpleDoc).toBeDefined()
|
||||
expect(logEntry).toBeDefined()
|
||||
expect((logEntry?.output as any)?.simpleID).toBe(simpleDoc?.id)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -54,6 +54,7 @@ export type SupportedTimezones =
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
@@ -102,6 +103,9 @@ export interface Config {
|
||||
CreateSimpleRetries0: TaskCreateSimpleRetries0;
|
||||
CreateSimpleWithDuplicateMessage: TaskCreateSimpleWithDuplicateMessage;
|
||||
ExternalTask: TaskExternalTask;
|
||||
ThrowError: TaskThrowError;
|
||||
ReturnError: TaskReturnError;
|
||||
ReturnCustomError: TaskReturnCustomError;
|
||||
inline: {
|
||||
input: unknown;
|
||||
output: unknown;
|
||||
@@ -119,11 +123,13 @@ export interface Config {
|
||||
workflowRetries2TasksRetriesUndefined: WorkflowWorkflowRetries2TasksRetriesUndefined;
|
||||
workflowRetries2TasksRetries0: WorkflowWorkflowRetries2TasksRetries0;
|
||||
inlineTaskTest: WorkflowInlineTaskTest;
|
||||
inlineTaskTestDelayed: WorkflowInlineTaskTestDelayed;
|
||||
externalWorkflow: WorkflowExternalWorkflow;
|
||||
retriesBackoffTest: WorkflowRetriesBackoffTest;
|
||||
subTask: WorkflowSubTask;
|
||||
subTaskFails: WorkflowSubTaskFails;
|
||||
longRunning: WorkflowLongRunning;
|
||||
parallelTask: WorkflowParallelTask;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -259,7 +265,10 @@ export interface PayloadJob {
|
||||
| 'CreateSimpleRetriesUndefined'
|
||||
| 'CreateSimpleRetries0'
|
||||
| 'CreateSimpleWithDuplicateMessage'
|
||||
| 'ExternalTask';
|
||||
| 'ExternalTask'
|
||||
| 'ThrowError'
|
||||
| 'ReturnError'
|
||||
| 'ReturnCustomError';
|
||||
taskID: string;
|
||||
input?:
|
||||
| {
|
||||
@@ -305,11 +314,13 @@ export interface PayloadJob {
|
||||
| 'workflowRetries2TasksRetriesUndefined'
|
||||
| 'workflowRetries2TasksRetries0'
|
||||
| 'inlineTaskTest'
|
||||
| 'inlineTaskTestDelayed'
|
||||
| 'externalWorkflow'
|
||||
| 'retriesBackoffTest'
|
||||
| 'subTask'
|
||||
| 'subTaskFails'
|
||||
| 'longRunning'
|
||||
| 'parallelTask'
|
||||
)
|
||||
| null;
|
||||
taskSlug?:
|
||||
@@ -322,6 +333,9 @@ export interface PayloadJob {
|
||||
| 'CreateSimpleRetries0'
|
||||
| 'CreateSimpleWithDuplicateMessage'
|
||||
| 'ExternalTask'
|
||||
| 'ThrowError'
|
||||
| 'ReturnError'
|
||||
| 'ReturnCustomError'
|
||||
)
|
||||
| null;
|
||||
queue?: string | null;
|
||||
@@ -583,6 +597,32 @@ export interface TaskExternalTask {
|
||||
simpleID: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "TaskThrowError".
|
||||
*/
|
||||
export interface TaskThrowError {
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "TaskReturnError".
|
||||
*/
|
||||
export interface TaskReturnError {
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "TaskReturnCustomError".
|
||||
*/
|
||||
export interface TaskReturnCustomError {
|
||||
input: {
|
||||
errorMessage: string;
|
||||
};
|
||||
output?: unknown;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "MyUpdatePostWorkflowType".
|
||||
@@ -684,6 +724,15 @@ export interface WorkflowInlineTaskTest {
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "WorkflowInlineTaskTestDelayed".
|
||||
*/
|
||||
export interface WorkflowInlineTaskTestDelayed {
|
||||
input: {
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "WorkflowExternalWorkflow".
|
||||
@@ -727,6 +776,13 @@ export interface WorkflowSubTaskFails {
|
||||
export interface WorkflowLongRunning {
|
||||
input?: unknown;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "WorkflowParallelTask".
|
||||
*/
|
||||
export interface WorkflowParallelTask {
|
||||
input?: unknown;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
|
||||
38
test/queues/workflows/inlineTaskTestDelayed.ts
Normal file
38
test/queues/workflows/inlineTaskTestDelayed.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { WorkflowConfig } from 'payload'
|
||||
|
||||
export const inlineTaskTestDelayedWorkflow: WorkflowConfig<'inlineTaskTestDelayed'> = {
|
||||
slug: 'inlineTaskTestDelayed',
|
||||
inputSchema: [
|
||||
{
|
||||
name: 'message',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
handler: async ({ job, inlineTask }) => {
|
||||
await inlineTask('1', {
|
||||
task: async ({ input, req }) => {
|
||||
// Wait 100ms
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
const newSimple = await req.payload.create({
|
||||
collection: 'simple',
|
||||
req,
|
||||
data: {
|
||||
title: input.message,
|
||||
},
|
||||
})
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
return {
|
||||
output: {
|
||||
simpleID: newSimple.id,
|
||||
},
|
||||
}
|
||||
},
|
||||
input: {
|
||||
message: job.input.message,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
29
test/queues/workflows/parallelTaskWorkflow.ts
Normal file
29
test/queues/workflows/parallelTaskWorkflow.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { WorkflowConfig } from 'payload'
|
||||
|
||||
export const parallelTaskWorkflow: WorkflowConfig<'parallelTask'> = {
|
||||
slug: 'parallelTask',
|
||||
inputSchema: [],
|
||||
handler: async ({ job, inlineTask }) => {
|
||||
const taskIDs = Array.from({ length: 500 }, (_, i) => i + 1).map((i) => i.toString())
|
||||
|
||||
await Promise.all(
|
||||
taskIDs.map(async (taskID) => {
|
||||
return await inlineTask(`parallel task ${taskID}`, {
|
||||
task: async ({ req }) => {
|
||||
const newSimple = await req.payload.db.create({
|
||||
collection: 'simple',
|
||||
data: {
|
||||
title: 'parallel task ' + taskID,
|
||||
},
|
||||
})
|
||||
return {
|
||||
output: {
|
||||
simpleID: newSimple.id,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -336,6 +336,41 @@ describe('Relationships', () => {
|
||||
expect(query.docs).toHaveLength(1) // Due to limit: 1
|
||||
})
|
||||
|
||||
it('should allow querying by relationships with an object where as AND', async () => {
|
||||
const director = await payload.create({
|
||||
collection: 'directors',
|
||||
data: { name: 'Director1', localized: 'Director1_Localized' },
|
||||
})
|
||||
|
||||
const movie = await payload.create({
|
||||
collection: 'movies',
|
||||
data: { director: director.id },
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
const { docs: trueRes } = await payload.find({
|
||||
collection: 'movies',
|
||||
depth: 0,
|
||||
where: {
|
||||
'director.name': { equals: 'Director1' },
|
||||
'director.localized': { equals: 'Director1_Localized' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(trueRes).toStrictEqual([movie])
|
||||
|
||||
const { docs: falseRes } = await payload.find({
|
||||
collection: 'movies',
|
||||
depth: 0,
|
||||
where: {
|
||||
'director.name': { equals: 'Director1_Fake' },
|
||||
'director.localized': { equals: 'Director1_Localized' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(falseRes).toStrictEqual([])
|
||||
})
|
||||
|
||||
it('should allow querying within blocks', async () => {
|
||||
const rel = await payload.create({
|
||||
collection: relationSlug,
|
||||
@@ -397,21 +432,7 @@ describe('Relationships', () => {
|
||||
expect(result.docs[0].id).toBe(id)
|
||||
})
|
||||
|
||||
describe('Custom ID', () => {
|
||||
it('should query a custom id relation', async () => {
|
||||
const { customIdRelation } = await restClient
|
||||
.GET(`/${slug}/${post.id}`)
|
||||
.then((res) => res.json())
|
||||
expect(customIdRelation).toMatchObject({ id: generatedCustomId })
|
||||
})
|
||||
|
||||
it('should query a custom id number relation', async () => {
|
||||
const { customIdNumberRelation } = await restClient
|
||||
.GET(`/${slug}/${post.id}`)
|
||||
.then((res) => res.json())
|
||||
expect(customIdNumberRelation).toMatchObject({ id: generatedCustomIdNumber })
|
||||
})
|
||||
|
||||
describe('hasMany relationships', () => {
|
||||
it('should retrieve totalDocs correctly with hasMany,', async () => {
|
||||
const movie1 = await payload.create({
|
||||
collection: 'movies',
|
||||
@@ -592,47 +613,59 @@ describe('Relationships', () => {
|
||||
expect(query1.totalDocs).toStrictEqual(1)
|
||||
})
|
||||
|
||||
it('should sort by a property of a hasMany relationship', async () => {
|
||||
const movie1 = await payload.create({
|
||||
collection: 'movies',
|
||||
it('should query using "in" by hasMany relationship field', async () => {
|
||||
const tree1 = await payload.create({
|
||||
collection: treeSlug,
|
||||
data: {
|
||||
name: 'Pulp Fiction',
|
||||
text: 'Tree 1',
|
||||
},
|
||||
})
|
||||
|
||||
const movie2 = await payload.create({
|
||||
collection: 'movies',
|
||||
const tree2 = await payload.create({
|
||||
collection: treeSlug,
|
||||
data: {
|
||||
name: 'Inception',
|
||||
parent: tree1.id,
|
||||
text: 'Tree 2',
|
||||
},
|
||||
})
|
||||
|
||||
await payload.delete({ collection: 'directors', where: {} })
|
||||
|
||||
const director1 = await payload.create({
|
||||
collection: 'directors',
|
||||
const tree3 = await payload.create({
|
||||
collection: treeSlug,
|
||||
data: {
|
||||
name: 'Quentin Tarantino',
|
||||
movies: [movie1.id],
|
||||
},
|
||||
})
|
||||
const director2 = await payload.create({
|
||||
collection: 'directors',
|
||||
data: {
|
||||
name: 'Christopher Nolan',
|
||||
movies: [movie2.id],
|
||||
parent: tree2.id,
|
||||
text: 'Tree 3',
|
||||
},
|
||||
})
|
||||
|
||||
const result = await payload.find({
|
||||
collection: 'directors',
|
||||
const tree4 = await payload.create({
|
||||
collection: treeSlug,
|
||||
data: {
|
||||
parent: tree3.id,
|
||||
text: 'Tree 4',
|
||||
},
|
||||
})
|
||||
|
||||
const validParents = [tree2.id, tree3.id]
|
||||
|
||||
const query = await payload.find({
|
||||
collection: treeSlug,
|
||||
depth: 0,
|
||||
sort: '-movies.name',
|
||||
sort: 'createdAt',
|
||||
where: {
|
||||
parent: {
|
||||
in: validParents,
|
||||
},
|
||||
},
|
||||
})
|
||||
// should only return tree3 and tree4
|
||||
|
||||
expect(result.docs[0].id).toStrictEqual(director1.id)
|
||||
expect(query.totalDocs).toEqual(2)
|
||||
expect(query.docs[0].text).toEqual('Tree 3')
|
||||
expect(query.docs[1].text).toEqual('Tree 4')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sorting by relationships', () => {
|
||||
it('should sort by a property of a relationship', async () => {
|
||||
await payload.delete({ collection: 'directors', where: {} })
|
||||
await payload.delete({ collection: 'movies', where: {} })
|
||||
@@ -719,236 +752,61 @@ describe('Relationships', () => {
|
||||
expect(localized_res_2.docs).toStrictEqual([movie_1, movie_2])
|
||||
})
|
||||
|
||||
it('should query using "in" by hasMany relationship field', async () => {
|
||||
const tree1 = await payload.create({
|
||||
collection: treeSlug,
|
||||
it('should sort by a property of a hasMany relationship', async () => {
|
||||
const movie1 = await payload.create({
|
||||
collection: 'movies',
|
||||
data: {
|
||||
text: 'Tree 1',
|
||||
name: 'Pulp Fiction',
|
||||
},
|
||||
})
|
||||
|
||||
const tree2 = await payload.create({
|
||||
collection: treeSlug,
|
||||
const movie2 = await payload.create({
|
||||
collection: 'movies',
|
||||
data: {
|
||||
parent: tree1.id,
|
||||
text: 'Tree 2',
|
||||
name: 'Inception',
|
||||
},
|
||||
})
|
||||
|
||||
const tree3 = await payload.create({
|
||||
collection: treeSlug,
|
||||
await payload.delete({ collection: 'directors', where: {} })
|
||||
|
||||
const director1 = await payload.create({
|
||||
collection: 'directors',
|
||||
data: {
|
||||
parent: tree2.id,
|
||||
text: 'Tree 3',
|
||||
name: 'Quentin Tarantino',
|
||||
movies: [movie1.id],
|
||||
},
|
||||
})
|
||||
const director2 = await payload.create({
|
||||
collection: 'directors',
|
||||
data: {
|
||||
name: 'Christopher Nolan',
|
||||
movies: [movie2.id],
|
||||
},
|
||||
})
|
||||
|
||||
const tree4 = await payload.create({
|
||||
collection: treeSlug,
|
||||
data: {
|
||||
parent: tree3.id,
|
||||
text: 'Tree 4',
|
||||
},
|
||||
})
|
||||
|
||||
const validParents = [tree2.id, tree3.id]
|
||||
|
||||
const query = await payload.find({
|
||||
collection: treeSlug,
|
||||
const result = await payload.find({
|
||||
collection: 'directors',
|
||||
depth: 0,
|
||||
sort: 'createdAt',
|
||||
where: {
|
||||
parent: {
|
||||
in: validParents,
|
||||
},
|
||||
},
|
||||
sort: '-movies.name',
|
||||
})
|
||||
// should only return tree3 and tree4
|
||||
|
||||
expect(query.totalDocs).toEqual(2)
|
||||
expect(query.docs[0].text).toEqual('Tree 3')
|
||||
expect(query.docs[1].text).toEqual('Tree 4')
|
||||
expect(result.docs[0].id).toStrictEqual(director1.id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Custom ID', () => {
|
||||
it('should query a custom id relation', async () => {
|
||||
const { customIdRelation } = await restClient
|
||||
.GET(`/${slug}/${post.id}`)
|
||||
.then((res) => res.json())
|
||||
expect(customIdRelation).toMatchObject({ id: generatedCustomId })
|
||||
})
|
||||
|
||||
it('should validate the format of text id relationships', async () => {
|
||||
await expect(async () =>
|
||||
createPost({
|
||||
// @ts-expect-error Sending bad data to test error handling
|
||||
customIdRelation: 1234,
|
||||
}),
|
||||
).rejects.toThrow('The following field is invalid: Custom Id Relation')
|
||||
})
|
||||
|
||||
it('should validate the format of number id relationships', async () => {
|
||||
await expect(async () =>
|
||||
createPost({
|
||||
// @ts-expect-error Sending bad data to test error handling
|
||||
customIdNumberRelation: 'bad-input',
|
||||
}),
|
||||
).rejects.toThrow('The following field is invalid: Custom Id Number Relation')
|
||||
})
|
||||
|
||||
it('should allow update removing a relationship', async () => {
|
||||
const response = await restClient.PATCH(`/${slug}/${post.id}`, {
|
||||
body: JSON.stringify({
|
||||
customIdRelation: null,
|
||||
relationField: null,
|
||||
}),
|
||||
})
|
||||
const doc = await response.json()
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(doc.relationField).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should query a polymorphic relationship field with mixed custom ids and default', async () => {
|
||||
const customIDNumber = await payload.create({
|
||||
collection: 'custom-id-number',
|
||||
data: { id: 999 },
|
||||
})
|
||||
|
||||
const customIDText = await payload.create({
|
||||
collection: 'custom-id',
|
||||
data: { id: 'custom-id' },
|
||||
})
|
||||
|
||||
const page = await payload.create({
|
||||
collection: 'pages',
|
||||
data: {},
|
||||
})
|
||||
|
||||
const relToCustomIdText = await payload.create({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
data: {
|
||||
rel: {
|
||||
relationTo: 'custom-id',
|
||||
value: customIDText.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const relToCustomIdNumber = await payload.create({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
data: {
|
||||
rel: {
|
||||
relationTo: 'custom-id-number',
|
||||
value: customIDNumber.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const relToPage = await payload.create({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
data: {
|
||||
rel: {
|
||||
relationTo: 'pages',
|
||||
value: page.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const pageResult = await payload.find({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
'rel.value': {
|
||||
equals: page.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'rel.relationTo': {
|
||||
equals: 'pages',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(pageResult.totalDocs).toBe(1)
|
||||
expect(pageResult.docs[0].id).toBe(relToPage.id)
|
||||
|
||||
const customIDResult = await payload.find({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
'rel.value': {
|
||||
equals: customIDText.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'rel.relationTo': {
|
||||
equals: 'custom-id',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(customIDResult.totalDocs).toBe(1)
|
||||
expect(customIDResult.docs[0].id).toBe(relToCustomIdText.id)
|
||||
|
||||
const customIDNumberResult = await payload.find({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
'rel.value': {
|
||||
equals: customIDNumber.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'rel.relationTo': {
|
||||
equals: 'custom-id-number',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(customIDNumberResult.totalDocs).toBe(1)
|
||||
expect(customIDNumberResult.docs[0].id).toBe(relToCustomIdNumber.id)
|
||||
|
||||
const inResult_1 = await payload.find({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
where: {
|
||||
'rel.value': {
|
||||
in: [page.id, customIDNumber.id],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(inResult_1.totalDocs).toBe(2)
|
||||
expect(inResult_1.docs.some((each) => each.id === relToPage.id)).toBeTruthy()
|
||||
expect(inResult_1.docs.some((each) => each.id === relToCustomIdNumber.id)).toBeTruthy()
|
||||
|
||||
const inResult_2 = await payload.find({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
where: {
|
||||
'rel.value': {
|
||||
in: [customIDNumber.id, customIDText.id],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(inResult_2.totalDocs).toBe(2)
|
||||
expect(inResult_2.docs.some((each) => each.id === relToCustomIdText.id)).toBeTruthy()
|
||||
expect(inResult_2.docs.some((each) => each.id === relToCustomIdNumber.id)).toBeTruthy()
|
||||
|
||||
const inResult_3 = await payload.find({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
where: {
|
||||
'rel.value': {
|
||||
in: [customIDNumber.id, customIDText.id, page.id],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(inResult_3.totalDocs).toBe(3)
|
||||
expect(inResult_3.docs.some((each) => each.id === relToCustomIdText.id)).toBeTruthy()
|
||||
expect(inResult_3.docs.some((each) => each.id === relToCustomIdNumber.id)).toBeTruthy()
|
||||
expect(inResult_3.docs.some((each) => each.id === relToPage.id)).toBeTruthy()
|
||||
it('should query a custom id number relation', async () => {
|
||||
const { customIdNumberRelation } = await restClient
|
||||
.GET(`/${slug}/${post.id}`)
|
||||
.then((res) => res.json())
|
||||
expect(customIdNumberRelation).toMatchObject({ id: generatedCustomIdNumber })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1072,6 +930,19 @@ describe('Relationships', () => {
|
||||
expect(docs.map((doc) => doc?.id)).not.toContain(localizedPost2.id)
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow update removing a relationship', async () => {
|
||||
const response = await restClient.PATCH(`/${slug}/${post.id}`, {
|
||||
body: JSON.stringify({
|
||||
customIdRelation: null,
|
||||
relationField: null,
|
||||
}),
|
||||
})
|
||||
const doc = await response.json()
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(doc.relationField).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Nested Querying', () => {
|
||||
@@ -1797,6 +1668,174 @@ describe('Relationships', () => {
|
||||
|
||||
expect(updated.status).toBe('completed')
|
||||
})
|
||||
|
||||
it('should validate the format of text id relationships', async () => {
|
||||
await expect(async () =>
|
||||
createPost({
|
||||
// @ts-expect-error Sending bad data to test error handling
|
||||
customIdRelation: 1234,
|
||||
}),
|
||||
).rejects.toThrow('The following field is invalid: Custom Id Relation')
|
||||
})
|
||||
|
||||
it('should validate the format of number id relationships', async () => {
|
||||
await expect(async () =>
|
||||
createPost({
|
||||
// @ts-expect-error Sending bad data to test error handling
|
||||
customIdNumberRelation: 'bad-input',
|
||||
}),
|
||||
).rejects.toThrow('The following field is invalid: Custom Id Number Relation')
|
||||
})
|
||||
|
||||
it('should query a polymorphic relationship field with mixed custom ids and default', async () => {
|
||||
const customIDNumber = await payload.create({
|
||||
collection: 'custom-id-number',
|
||||
data: { id: 999 },
|
||||
})
|
||||
|
||||
const customIDText = await payload.create({
|
||||
collection: 'custom-id',
|
||||
data: { id: 'custom-id' },
|
||||
})
|
||||
|
||||
const page = await payload.create({
|
||||
collection: 'pages',
|
||||
data: {},
|
||||
})
|
||||
|
||||
const relToCustomIdText = await payload.create({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
data: {
|
||||
rel: {
|
||||
relationTo: 'custom-id',
|
||||
value: customIDText.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const relToCustomIdNumber = await payload.create({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
data: {
|
||||
rel: {
|
||||
relationTo: 'custom-id-number',
|
||||
value: customIDNumber.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const relToPage = await payload.create({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
data: {
|
||||
rel: {
|
||||
relationTo: 'pages',
|
||||
value: page.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const pageResult = await payload.find({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
'rel.value': {
|
||||
equals: page.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'rel.relationTo': {
|
||||
equals: 'pages',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(pageResult.totalDocs).toBe(1)
|
||||
expect(pageResult.docs[0].id).toBe(relToPage.id)
|
||||
|
||||
const customIDResult = await payload.find({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
'rel.value': {
|
||||
equals: customIDText.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'rel.relationTo': {
|
||||
equals: 'custom-id',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(customIDResult.totalDocs).toBe(1)
|
||||
expect(customIDResult.docs[0].id).toBe(relToCustomIdText.id)
|
||||
|
||||
const customIDNumberResult = await payload.find({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
'rel.value': {
|
||||
equals: customIDNumber.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
'rel.relationTo': {
|
||||
equals: 'custom-id-number',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(customIDNumberResult.totalDocs).toBe(1)
|
||||
expect(customIDNumberResult.docs[0].id).toBe(relToCustomIdNumber.id)
|
||||
|
||||
const inResult_1 = await payload.find({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
where: {
|
||||
'rel.value': {
|
||||
in: [page.id, customIDNumber.id],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(inResult_1.totalDocs).toBe(2)
|
||||
expect(inResult_1.docs.some((each) => each.id === relToPage.id)).toBeTruthy()
|
||||
expect(inResult_1.docs.some((each) => each.id === relToCustomIdNumber.id)).toBeTruthy()
|
||||
|
||||
const inResult_2 = await payload.find({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
where: {
|
||||
'rel.value': {
|
||||
in: [customIDNumber.id, customIDText.id],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(inResult_2.totalDocs).toBe(2)
|
||||
expect(inResult_2.docs.some((each) => each.id === relToCustomIdText.id)).toBeTruthy()
|
||||
expect(inResult_2.docs.some((each) => each.id === relToCustomIdNumber.id)).toBeTruthy()
|
||||
|
||||
const inResult_3 = await payload.find({
|
||||
collection: 'rels-to-pages-and-custom-text-ids',
|
||||
where: {
|
||||
'rel.value': {
|
||||
in: [customIDNumber.id, customIDText.id, page.id],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(inResult_3.totalDocs).toBe(3)
|
||||
expect(inResult_3.docs.some((each) => each.id === relToCustomIdText.id)).toBeTruthy()
|
||||
expect(inResult_3.docs.some((each) => each.id === relToCustomIdNumber.id)).toBeTruthy()
|
||||
expect(inResult_3.docs.some((each) => each.id === relToPage.id)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
19
test/sort/Seed.tsx
Normal file
19
test/sort/Seed.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
/* eslint-disable no-console */
|
||||
'use client'
|
||||
|
||||
export const Seed = () => {
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await fetch('/api/seed', { method: 'POST' })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Seed
|
||||
</button>
|
||||
)
|
||||
}
|
||||
27
test/sort/collections/Orderable/index.ts
Normal file
27
test/sort/collections/Orderable/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { orderableJoinSlug } from '../OrderableJoin/index.js'
|
||||
|
||||
export const orderableSlug = 'orderable'
|
||||
|
||||
export const OrderableCollection: CollectionConfig = {
|
||||
slug: orderableSlug,
|
||||
orderable: true,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
components: {
|
||||
beforeList: ['/Seed.tsx#Seed'],
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'orderableField',
|
||||
type: 'relationship',
|
||||
relationTo: orderableJoinSlug,
|
||||
},
|
||||
],
|
||||
}
|
||||
39
test/sort/collections/OrderableJoin/index.ts
Normal file
39
test/sort/collections/OrderableJoin/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const orderableJoinSlug = 'orderable-join'
|
||||
|
||||
export const OrderableJoinCollection: CollectionConfig = {
|
||||
slug: orderableJoinSlug,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
components: {
|
||||
beforeList: ['/Seed.tsx#Seed'],
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'orderableJoinField1',
|
||||
type: 'join',
|
||||
on: 'orderableField',
|
||||
orderable: true,
|
||||
collection: 'orderable',
|
||||
},
|
||||
{
|
||||
name: 'orderableJoinField2',
|
||||
type: 'join',
|
||||
on: 'orderableField',
|
||||
orderable: true,
|
||||
collection: 'orderable',
|
||||
},
|
||||
{
|
||||
name: 'nonOrderableJoinField',
|
||||
type: 'join',
|
||||
on: 'orderableField',
|
||||
collection: 'orderable',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { CollectionSlug, Payload } from 'payload'
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
@@ -6,17 +8,39 @@ import { devUser } from '../credentials.js'
|
||||
import { DefaultSortCollection } from './collections/DefaultSort/index.js'
|
||||
import { DraftsCollection } from './collections/Drafts/index.js'
|
||||
import { LocalizedCollection } from './collections/Localized/index.js'
|
||||
import { OrderableCollection } from './collections/Orderable/index.js'
|
||||
import { OrderableJoinCollection } from './collections/OrderableJoin/index.js'
|
||||
import { PostsCollection } from './collections/Posts/index.js'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [PostsCollection, DraftsCollection, DefaultSortCollection, LocalizedCollection],
|
||||
collections: [
|
||||
PostsCollection,
|
||||
DraftsCollection,
|
||||
DefaultSortCollection,
|
||||
LocalizedCollection,
|
||||
OrderableCollection,
|
||||
OrderableJoinCollection,
|
||||
],
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
endpoints: [
|
||||
{
|
||||
path: '/seed',
|
||||
method: 'post',
|
||||
handler: async (req) => {
|
||||
await seedSortable(req.payload)
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 200,
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
cors: ['http://localhost:3000', 'http://localhost:3001'],
|
||||
localization: {
|
||||
locales: ['en', 'nb'],
|
||||
@@ -30,8 +54,41 @@ export default buildConfigWithDefaults({
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
await seedSortable(payload)
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
|
||||
export async function createData(
|
||||
payload: Payload,
|
||||
collection: CollectionSlug,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: Record<string, any>[],
|
||||
) {
|
||||
for (const item of data) {
|
||||
await payload.create({ collection, data: item })
|
||||
}
|
||||
}
|
||||
|
||||
async function seedSortable(payload: Payload) {
|
||||
await payload.delete({ collection: 'orderable', where: {} })
|
||||
await payload.delete({ collection: 'orderable-join', where: {} })
|
||||
|
||||
const joinA = await payload.create({ collection: 'orderable-join', data: { title: 'Join A' } })
|
||||
|
||||
await createData(payload, 'orderable', [
|
||||
{ title: 'A', orderableField: joinA.id },
|
||||
{ title: 'B', orderableField: joinA.id },
|
||||
{ title: 'C', orderableField: joinA.id },
|
||||
{ title: 'D', orderableField: joinA.id },
|
||||
])
|
||||
|
||||
await payload.create({ collection: 'orderable-join', data: { title: 'Join B' } })
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
status: 200,
|
||||
})
|
||||
}
|
||||
|
||||
152
test/sort/e2e.spec.ts
Normal file
152
test/sort/e2e.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { BrowserContext, Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { RESTClient } from 'helpers/rest.js'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
||||
import type { Config } from './payload-types.js'
|
||||
|
||||
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
import { orderableSlug } from './collections/Orderable/index.js'
|
||||
import { orderableJoinSlug } from './collections/OrderableJoin/index.js'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const { beforeAll, describe } = test
|
||||
let page: Page
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
let payload: PayloadTestSDK<Config>
|
||||
let client: RESTClient
|
||||
let serverURL: string
|
||||
let context: BrowserContext
|
||||
|
||||
describe('Sort functionality', () => {
|
||||
beforeAll(async ({ browser }, testInfo) => {
|
||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({ dirname }))
|
||||
|
||||
context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
|
||||
initPageConsoleErrorCatch(page)
|
||||
|
||||
client = new RESTClient({ defaultSlug: 'users', serverURL })
|
||||
await client.login()
|
||||
|
||||
await ensureCompilationIsDone({ page, serverURL })
|
||||
})
|
||||
|
||||
// NOTES: It works for me in headed browser but not in headless, I don't know why.
|
||||
// If you are debugging this test, remember to press the seed button before each attempt.
|
||||
// assertRows contains expect
|
||||
// eslint-disable-next-line playwright/expect-expect
|
||||
test('Orderable collection', async () => {
|
||||
const url = new AdminUrlUtil(serverURL, orderableSlug)
|
||||
await page.goto(`${url.list}?sort=-_order`)
|
||||
// SORT BY ORDER ASCENDING
|
||||
await page.locator('.sort-header button').nth(0).click()
|
||||
await assertRows(0, 'A', 'B', 'C', 'D')
|
||||
await moveRow(2, 3) // move to middle
|
||||
await assertRows(0, 'A', 'C', 'B', 'D')
|
||||
await moveRow(3, 1) // move to top
|
||||
await assertRows(0, 'B', 'A', 'C', 'D')
|
||||
await moveRow(1, 4) // move to bottom
|
||||
await assertRows(0, 'A', 'C', 'D', 'B')
|
||||
|
||||
// SORT BY ORDER DESCENDING
|
||||
await page.locator('.sort-header button').nth(0).click()
|
||||
await page.waitForURL(/sort=-_order/, { timeout: 2000 })
|
||||
await assertRows(0, 'B', 'D', 'C', 'A')
|
||||
await moveRow(1, 3) // move to middle
|
||||
await assertRows(0, 'D', 'C', 'B', 'A')
|
||||
await moveRow(3, 1) // move to top
|
||||
await assertRows(0, 'B', 'D', 'C', 'A')
|
||||
await moveRow(1, 4) // move to bottom
|
||||
await assertRows(0, 'D', 'C', 'A', 'B')
|
||||
|
||||
// SORT BY TITLE
|
||||
await page.getByLabel('Sort by Title Ascending').click()
|
||||
await page.waitForURL(/sort=title/, { timeout: 2000 })
|
||||
await moveRow(1, 3, 'warning') // warning because not sorted by order first
|
||||
})
|
||||
|
||||
test('Orderable join fields', async () => {
|
||||
const url = new AdminUrlUtil(serverURL, orderableJoinSlug)
|
||||
await page.goto(url.list)
|
||||
|
||||
await page.getByText('Join A').click()
|
||||
await expect(page.locator('.sort-header button')).toHaveCount(2)
|
||||
|
||||
await page.locator('.sort-header button').nth(0).click()
|
||||
await assertRows(0, 'A', 'B', 'C', 'D')
|
||||
await moveRow(2, 3, 'success', 0) // move to middle
|
||||
await assertRows(0, 'A', 'C', 'B', 'D')
|
||||
|
||||
await page.locator('.sort-header button').nth(1).click()
|
||||
await assertRows(1, 'A', 'B', 'C', 'D')
|
||||
await moveRow(1, 4, 'success', 1) // move to end
|
||||
await assertRows(1, 'B', 'C', 'D', 'A')
|
||||
|
||||
await page.reload()
|
||||
await page.locator('.sort-header button').nth(0).click()
|
||||
await page.locator('.sort-header button').nth(1).click()
|
||||
await assertRows(0, 'A', 'C', 'B', 'D')
|
||||
await assertRows(1, 'B', 'C', 'D', 'A')
|
||||
})
|
||||
})
|
||||
|
||||
async function moveRow(
|
||||
from: number,
|
||||
to: number,
|
||||
expected: 'success' | 'warning' = 'success',
|
||||
nthTable = 0,
|
||||
) {
|
||||
// counting from 1, zero excluded
|
||||
const table = page.locator(`tbody`).nth(nthTable)
|
||||
const dragHandle = table.locator(`.sort-row`)
|
||||
const source = dragHandle.nth(from - 1)
|
||||
const target = dragHandle.nth(to - 1)
|
||||
|
||||
const sourceBox = await source.boundingBox()
|
||||
const targetBox = await target.boundingBox()
|
||||
if (!sourceBox || !targetBox) {
|
||||
throw new Error(
|
||||
`Could not find elements to DnD. Probably the dndkit animation is not finished. Try increasing the timeout`,
|
||||
)
|
||||
}
|
||||
// steps is important: move slightly to trigger the drag sensor of DnD-kit
|
||||
await page.mouse.move(sourceBox.x + sourceBox.width / 2, sourceBox.y + sourceBox.height / 2, {
|
||||
steps: 10,
|
||||
})
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(targetBox.x + targetBox.width / 2, targetBox.y + targetBox.height / 2, {
|
||||
steps: 10,
|
||||
})
|
||||
await page.mouse.up()
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(400) // dndkit animation
|
||||
|
||||
if (expected === 'warning') {
|
||||
const toast = page.locator('.payload-toast-item.toast-warning')
|
||||
await expect(toast).toHaveText(
|
||||
'To reorder the rows you must first sort them by the "Order" column',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function assertRows(nthTable: number, ...expectedRows: Array<string>) {
|
||||
const table = page.locator('tbody').nth(nthTable)
|
||||
const cellTitle = table.locator('.cell-title > :first-child')
|
||||
|
||||
const rows = table.locator('.sort-row')
|
||||
await expect.poll(() => rows.count()).toBe(expectedRows.length)
|
||||
|
||||
for (let i = 0; i < expectedRows.length; i++) {
|
||||
await expect(cellTitle.nth(i)).toHaveText(expectedRows[i]!)
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,11 @@ import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||
import type { Orderable, OrderableJoin } from './payload-types.js'
|
||||
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import { orderableSlug } from './collections/Orderable/index.js'
|
||||
import { orderableJoinSlug } from './collections/OrderableJoin/index.js'
|
||||
|
||||
let payload: Payload
|
||||
let restClient: NextRESTClient
|
||||
@@ -15,8 +18,8 @@ const dirname = path.dirname(filename)
|
||||
|
||||
describe('Sort', () => {
|
||||
beforeAll(async () => {
|
||||
const initialized = await initPayloadInt(dirname)
|
||||
;({ payload, restClient } = initialized)
|
||||
// @ts-expect-error: initPayloadInt does not have a proper type definition
|
||||
;({ payload, restClient } = await initPayloadInt(dirname))
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -63,7 +66,7 @@ describe('Sort', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Sinlge sort field', () => {
|
||||
describe('Single sort field', () => {
|
||||
it('should sort posts by text field', async () => {
|
||||
const posts = await payload.find({
|
||||
collection: 'posts',
|
||||
@@ -326,6 +329,84 @@ describe('Sort', () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Orderable join', () => {
|
||||
let related: OrderableJoin
|
||||
let orderable1: Orderable
|
||||
let orderable2: Orderable
|
||||
let orderable3: Orderable
|
||||
|
||||
beforeAll(async () => {
|
||||
related = await payload.create({
|
||||
collection: orderableJoinSlug,
|
||||
data: {
|
||||
title: 'test',
|
||||
},
|
||||
})
|
||||
orderable1 = await payload.create({
|
||||
collection: orderableSlug,
|
||||
data: {
|
||||
title: 'test 1',
|
||||
orderableField: related.id,
|
||||
},
|
||||
})
|
||||
|
||||
orderable2 = await payload.create({
|
||||
collection: orderableSlug,
|
||||
data: {
|
||||
title: 'test 2',
|
||||
orderableField: related.id,
|
||||
},
|
||||
})
|
||||
|
||||
orderable3 = await payload.create({
|
||||
collection: orderableSlug,
|
||||
data: {
|
||||
title: 'test 3',
|
||||
orderableField: related.id,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should set order by default', () => {
|
||||
expect(orderable1._orderable_orderableJoinField1_order).toBeDefined()
|
||||
})
|
||||
|
||||
it('should allow setting the order with the local API', async () => {
|
||||
// create two orderableJoinSlug docs
|
||||
orderable2 = await payload.update({
|
||||
collection: orderableSlug,
|
||||
id: orderable2.id,
|
||||
data: {
|
||||
title: 'test',
|
||||
orderableField: related.id,
|
||||
_orderable_orderableJoinField1_order: 'e4',
|
||||
},
|
||||
})
|
||||
const orderable4 = await payload.create({
|
||||
collection: orderableSlug,
|
||||
data: {
|
||||
title: 'test',
|
||||
orderableField: related.id,
|
||||
_orderable_orderableJoinField1_order: 'e2',
|
||||
},
|
||||
})
|
||||
expect(orderable2._orderable_orderableJoinField1_order).toBe('e4')
|
||||
expect(orderable4._orderable_orderableJoinField1_order).toBe('e2')
|
||||
})
|
||||
it('should sort join docs in the correct', async () => {
|
||||
related = await payload.findByID({
|
||||
collection: orderableJoinSlug,
|
||||
id: related.id,
|
||||
depth: 1,
|
||||
})
|
||||
const orders = (related.orderableJoinField1 as { docs: Orderable[] }).docs.map((doc) =>
|
||||
parseInt(doc._orderable_orderableJoinField1_order, 16),
|
||||
) as [number, number, number]
|
||||
expect(orders[0]).toBeLessThan(orders[1])
|
||||
expect(orders[1]).toBeLessThan(orders[2])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('REST API', () => {
|
||||
@@ -344,7 +425,7 @@ describe('Sort', () => {
|
||||
await payload.delete({ collection: 'posts', where: {} })
|
||||
})
|
||||
|
||||
describe('Sinlge sort field', () => {
|
||||
describe('Single sort field', () => {
|
||||
it('should sort posts by text field', async () => {
|
||||
const res = await restClient
|
||||
.GET(`/posts`, {
|
||||
|
||||
@@ -54,6 +54,7 @@ export type SupportedTimezones =
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
@@ -70,17 +71,27 @@ export interface Config {
|
||||
drafts: Draft;
|
||||
'default-sort': DefaultSort;
|
||||
localized: Localized;
|
||||
orderable: Orderable;
|
||||
'orderable-join': OrderableJoin;
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsJoins: {
|
||||
'orderable-join': {
|
||||
orderableJoinField1: 'orderable';
|
||||
orderableJoinField2: 'orderable';
|
||||
nonOrderableJoinField: 'orderable';
|
||||
};
|
||||
};
|
||||
collectionsSelect: {
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
drafts: DraftsSelect<false> | DraftsSelect<true>;
|
||||
'default-sort': DefaultSortSelect<false> | DefaultSortSelect<true>;
|
||||
localized: LocalizedSelect<false> | LocalizedSelect<true>;
|
||||
orderable: OrderableSelect<false> | OrderableSelect<true>;
|
||||
'orderable-join': OrderableJoinSelect<false> | OrderableJoinSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
@@ -174,6 +185,45 @@ export interface Localized {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "orderable".
|
||||
*/
|
||||
export interface Orderable {
|
||||
id: string;
|
||||
_orderable_orderableJoinField2_order?: string;
|
||||
_orderable_orderableJoinField1_order?: string;
|
||||
_order?: string;
|
||||
title?: string | null;
|
||||
orderableField?: (string | null) | OrderableJoin;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "orderable-join".
|
||||
*/
|
||||
export interface OrderableJoin {
|
||||
id: string;
|
||||
title?: string | null;
|
||||
orderableJoinField1?: {
|
||||
docs?: (string | Orderable)[];
|
||||
hasNextPage?: boolean;
|
||||
totalDocs?: number;
|
||||
};
|
||||
orderableJoinField2?: {
|
||||
docs?: (string | Orderable)[];
|
||||
hasNextPage?: boolean;
|
||||
totalDocs?: number;
|
||||
};
|
||||
nonOrderableJoinField?: {
|
||||
docs?: (string | Orderable)[];
|
||||
hasNextPage?: boolean;
|
||||
totalDocs?: number;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
@@ -214,6 +264,14 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'localized';
|
||||
value: string | Localized;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'orderable';
|
||||
value: string | Orderable;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'orderable-join';
|
||||
value: string | OrderableJoin;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
@@ -316,6 +374,31 @@ export interface LocalizedSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "orderable_select".
|
||||
*/
|
||||
export interface OrderableSelect<T extends boolean = true> {
|
||||
_orderable_orderableJoinField2_order?: T;
|
||||
_orderable_orderableJoinField1_order?: T;
|
||||
_order?: T;
|
||||
title?: T;
|
||||
orderableField?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "orderable-join_select".
|
||||
*/
|
||||
export interface OrderableJoinSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
orderableJoinField1?: T;
|
||||
orderableJoinField2?: T;
|
||||
nonOrderableJoinField?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
|
||||
@@ -735,6 +735,31 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'best-fit',
|
||||
fields: [
|
||||
{
|
||||
name: 'withAdminThumbnail',
|
||||
type: 'upload',
|
||||
relationTo: 'admin-thumbnail-function',
|
||||
},
|
||||
{
|
||||
name: 'withinRange',
|
||||
type: 'upload',
|
||||
relationTo: enlargeSlug,
|
||||
},
|
||||
{
|
||||
name: 'nextSmallestOutOfRange',
|
||||
type: 'upload',
|
||||
relationTo: 'focal-only',
|
||||
},
|
||||
{
|
||||
name: 'original',
|
||||
type: 'upload',
|
||||
relationTo: 'focal-only',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
onInit: async (payload) => {
|
||||
const uploadsDir = path.resolve(dirname, './media')
|
||||
|
||||
@@ -66,6 +66,7 @@ let uploadsOne: AdminUrlUtil
|
||||
let uploadsTwo: AdminUrlUtil
|
||||
let customUploadFieldURL: AdminUrlUtil
|
||||
let hideFileInputOnCreateURL: AdminUrlUtil
|
||||
let bestFitURL: AdminUrlUtil
|
||||
let consoleErrorsFromPage: string[] = []
|
||||
let collectErrorsFromPage: () => boolean
|
||||
let stopCollectingErrorsFromPage: () => boolean
|
||||
@@ -99,6 +100,7 @@ describe('Uploads', () => {
|
||||
uploadsTwo = new AdminUrlUtil(serverURL, 'uploads-2')
|
||||
customUploadFieldURL = new AdminUrlUtil(serverURL, customUploadFieldSlug)
|
||||
hideFileInputOnCreateURL = new AdminUrlUtil(serverURL, hideFileInputOnCreateSlug)
|
||||
bestFitURL = new AdminUrlUtil(serverURL, 'best-fit')
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
@@ -1349,4 +1351,53 @@ describe('Uploads', () => {
|
||||
|
||||
await expect(page.locator('.file-field .file-details__remove')).toBeHidden()
|
||||
})
|
||||
|
||||
describe('imageSizes best fit', () => {
|
||||
test('should select adminThumbnail if one exists', async () => {
|
||||
await page.goto(bestFitURL.create)
|
||||
await page.locator('#field-withAdminThumbnail button.upload__listToggler').click()
|
||||
await page.locator('tr.row-1 td.cell-filename button.default-cell__first-cell').click()
|
||||
const thumbnail = page.locator('#field-withAdminThumbnail div.thumbnail > img')
|
||||
await expect(thumbnail).toHaveAttribute(
|
||||
'src',
|
||||
'https://payloadcms.com/images/universal-truth.jpg',
|
||||
)
|
||||
})
|
||||
|
||||
test('should select an image within target range', async () => {
|
||||
await page.goto(bestFitURL.create)
|
||||
await page.locator('#field-withinRange button.upload__createNewToggler').click()
|
||||
const fileChooserPromise = page.waitForEvent('filechooser')
|
||||
await page.getByText('Select a file').click()
|
||||
const fileChooser = await fileChooserPromise
|
||||
await fileChooser.setFiles(path.join(dirname, 'test-image.jpg'))
|
||||
await page.locator('dialog button#action-save').click()
|
||||
const thumbnail = page.locator('#field-withinRange div.thumbnail > img')
|
||||
await expect(thumbnail).toHaveAttribute('src', '/api/enlarge/file/test-image-180x50.jpg')
|
||||
})
|
||||
|
||||
test('should select next smallest image outside of range but smaller than original', async () => {
|
||||
await page.goto(bestFitURL.create)
|
||||
await page.locator('#field-nextSmallestOutOfRange button.upload__createNewToggler').click()
|
||||
const fileChooserPromise = page.waitForEvent('filechooser')
|
||||
await page.getByText('Select a file').click()
|
||||
const fileChooser = await fileChooserPromise
|
||||
await fileChooser.setFiles(path.join(dirname, 'test-image.jpg'))
|
||||
await page.locator('dialog button#action-save').click()
|
||||
const thumbnail = page.locator('#field-nextSmallestOutOfRange div.thumbnail > img')
|
||||
await expect(thumbnail).toHaveAttribute('src', '/api/focal-only/file/test-image-400x300.jpg')
|
||||
})
|
||||
|
||||
test('should select original if smaller than next available size', async () => {
|
||||
await page.goto(bestFitURL.create)
|
||||
await page.locator('#field-original button.upload__createNewToggler').click()
|
||||
const fileChooserPromise = page.waitForEvent('filechooser')
|
||||
await page.getByText('Select a file').click()
|
||||
const fileChooser = await fileChooserPromise
|
||||
await fileChooser.setFiles(path.join(dirname, 'small.png'))
|
||||
await page.locator('dialog button#action-save').click()
|
||||
const thumbnail = page.locator('#field-original div.thumbnail > img')
|
||||
await expect(thumbnail).toHaveAttribute('src', '/api/focal-only/file/small.png')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -101,6 +101,7 @@ export interface Config {
|
||||
'media-without-relation-preview': MediaWithoutRelationPreview;
|
||||
'relation-preview': RelationPreview;
|
||||
'hide-file-input-on-create': HideFileInputOnCreate;
|
||||
'best-fit': BestFit;
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
@@ -142,6 +143,7 @@ export interface Config {
|
||||
'media-without-relation-preview': MediaWithoutRelationPreviewSelect<false> | MediaWithoutRelationPreviewSelect<true>;
|
||||
'relation-preview': RelationPreviewSelect<false> | RelationPreviewSelect<true>;
|
||||
'hide-file-input-on-create': HideFileInputOnCreateSelect<false> | HideFileInputOnCreateSelect<true>;
|
||||
'best-fit': BestFitSelect<false> | BestFitSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
@@ -1260,6 +1262,19 @@ export interface RelationPreview {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "best-fit".
|
||||
*/
|
||||
export interface BestFit {
|
||||
id: string;
|
||||
withAdminThumbnail?: (string | null) | AdminThumbnailFunction;
|
||||
withinRange?: (string | null) | Enlarge;
|
||||
nextSmallestOutOfRange?: (string | null) | FocalOnly;
|
||||
original?: (string | null) | FocalOnly;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
@@ -1420,6 +1435,10 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'hide-file-input-on-create';
|
||||
value: string | HideFileInputOnCreate;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'best-fit';
|
||||
value: string | BestFit;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
@@ -2632,6 +2651,18 @@ export interface HideFileInputOnCreateSelect<T extends boolean = true> {
|
||||
focalX?: T;
|
||||
focalY?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "best-fit_select".
|
||||
*/
|
||||
export interface BestFitSelect<T extends boolean = true> {
|
||||
withAdminThumbnail?: T;
|
||||
withinRange?: T;
|
||||
nextSmallestOutOfRange?: T;
|
||||
original?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
|
||||
628
test/versions/collections/Diff/generateLexicalData.ts
Normal file
628
test/versions/collections/Diff/generateLexicalData.ts
Normal file
@@ -0,0 +1,628 @@
|
||||
import type { DefaultTypedEditorState, SerializedBlockNode } from '@payloadcms/richtext-lexical'
|
||||
|
||||
import { mediaCollectionSlug, textCollectionSlug } from '../../slugs.js'
|
||||
|
||||
export function generateLexicalData(args: {
|
||||
mediaID: number | string
|
||||
textID: number | string
|
||||
updated: boolean
|
||||
}): DefaultTypedEditorState {
|
||||
return {
|
||||
root: {
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: `Fugiat esse${args.updated ? ' new ' : ''}in dolor aleiqua ${args.updated ? 'gillum' : 'cillum'} proident ad cillum excepteur mollit reprehenderit mollit commodo. Pariatur incididunt non exercitation est mollit nisi labore${args.updated ? ' ' : 'delete'}officia cupidatat amet commodo commodo proident occaecat.`,
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: args.updated ? 1 : 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Some ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: args.updated ? 0 : 1,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Bold',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' and ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 1,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Italic',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' text with ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'a link',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'link',
|
||||
version: 3,
|
||||
fields: {
|
||||
url: args.updated ? 'https://www.payloadcms.com' : 'https://www.google.com',
|
||||
newTab: true,
|
||||
linkType: 'custom',
|
||||
},
|
||||
id: '67d869aa706b36f346ecffd9',
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' and ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'another link',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'link',
|
||||
version: 3,
|
||||
fields: {
|
||||
url: 'https://www.payload.ai',
|
||||
newTab: args.updated ? true : false,
|
||||
linkType: 'custom',
|
||||
},
|
||||
id: '67d869aa706b36f346ecffd0',
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: ' text ',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: args.updated ? 'third link updated' : 'third link',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'link',
|
||||
version: 3,
|
||||
fields: {
|
||||
url: 'https://www.payloadcms.com/docs',
|
||||
newTab: true,
|
||||
linkType: 'custom',
|
||||
},
|
||||
id: '67d869aa706b36f346ecffd0',
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: '.',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'link with description',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'link',
|
||||
version: 3,
|
||||
fields: {
|
||||
url: 'https://www.payloadcms.com/docs',
|
||||
description: args.updated ? 'updated description' : 'description',
|
||||
newTab: true,
|
||||
linkType: 'custom',
|
||||
},
|
||||
id: '67d869aa706b36f346ecffd0',
|
||||
},
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'text',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'identical link',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'link',
|
||||
version: 3,
|
||||
fields: {
|
||||
url: 'https://www.payloadcms.com/docs2',
|
||||
description: 'description',
|
||||
newTab: true,
|
||||
linkType: 'custom',
|
||||
},
|
||||
id: '67d869aa706b36f346ecffd0',
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'paragraph',
|
||||
version: 1,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'One',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: args.updated ? 'Two updated' : 'Two',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Three',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
value: 3,
|
||||
},
|
||||
...(args.updated
|
||||
? [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Four',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
value: 4,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'list',
|
||||
version: 1,
|
||||
listType: 'number',
|
||||
start: 1,
|
||||
tag: 'ol',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'One',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Two',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: args.updated ? 'Three' : 'Three original',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
value: 3,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'list',
|
||||
version: 1,
|
||||
listType: 'bullet',
|
||||
start: 1,
|
||||
tag: 'ul',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Checked',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
checked: true,
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: 'Unchecked',
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'listitem',
|
||||
version: 1,
|
||||
checked: args.updated ? false : true,
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'list',
|
||||
version: 1,
|
||||
listType: 'check',
|
||||
start: 1,
|
||||
tag: 'ul',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: `Heading1${args.updated ? ' updated' : ''}`,
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'heading',
|
||||
version: 1,
|
||||
tag: 'h1',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: `Heading2${args.updated ? ' updated' : ''}`,
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'heading',
|
||||
version: 1,
|
||||
tag: 'h2',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: `Heading3${args.updated ? ' updated' : ''}`,
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'heading',
|
||||
version: 1,
|
||||
tag: 'h3',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: `Heading4${args.updated ? ' updated' : ''}`,
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'heading',
|
||||
version: 1,
|
||||
tag: 'h4',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: `Heading5${args.updated ? ' updated' : ''}`,
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'heading',
|
||||
version: 1,
|
||||
tag: 'h5',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: `Heading6${args.updated ? ' updated' : ''}`,
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'heading',
|
||||
version: 1,
|
||||
tag: 'h6',
|
||||
},
|
||||
{
|
||||
type: 'upload',
|
||||
version: 3,
|
||||
format: '',
|
||||
id: '67d8693c76b36f346ecffd8',
|
||||
relationTo: mediaCollectionSlug,
|
||||
value: args.mediaID,
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text: `Quote${args.updated ? ' updated' : ''}`,
|
||||
type: 'text',
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'quote',
|
||||
version: 1,
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
version: 2,
|
||||
format: '',
|
||||
relationTo: textCollectionSlug,
|
||||
value: args.textID,
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
version: 2,
|
||||
format: '',
|
||||
fields: {
|
||||
id: '67d8693c706b36f346ecffd7',
|
||||
radios: args.updated ? 'option1' : 'option3',
|
||||
someText: `Text1${args.updated ? ' updated' : ''}`,
|
||||
blockName: '',
|
||||
someTextRequired: 'Text2',
|
||||
blockType: 'myBlock',
|
||||
},
|
||||
} as SerializedBlockNode,
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
type: 'root',
|
||||
version: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { diffCollectionSlug, draftCollectionSlug } from '../slugs.js'
|
||||
import { diffCollectionSlug, draftCollectionSlug } from '../../slugs.js'
|
||||
|
||||
export const Diff: CollectionConfig = {
|
||||
slug: diffCollectionSlug,
|
||||
@@ -39,6 +39,64 @@ export const Diff: CollectionConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'CollapsibleBlock',
|
||||
fields: [
|
||||
{
|
||||
type: 'collapsible',
|
||||
label: 'Collapsible',
|
||||
fields: [
|
||||
{
|
||||
type: 'collapsible',
|
||||
label: 'Nested Collapsible',
|
||||
fields: [
|
||||
{
|
||||
name: 'textInCollapsibleInCollapsibleBlock',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'textInRowInCollapsibleBlock',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'TabsBlock',
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'namedTab1InBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'textInNamedTab1InBlock',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Unnamed Tab 2 In Block',
|
||||
fields: [
|
||||
{
|
||||
name: 'textInUnnamedTab2InBlock',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
17
test/versions/collections/Text.ts
Normal file
17
test/versions/collections/Text.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { textCollectionSlug } from '../slugs.js'
|
||||
|
||||
export const TextCollection: CollectionConfig = {
|
||||
slug: textCollectionSlug,
|
||||
admin: {
|
||||
useAsTitle: 'text',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import AutosavePosts from './collections/Autosave.js'
|
||||
import AutosaveWithValidate from './collections/AutosaveWithValidate.js'
|
||||
import CustomIDs from './collections/CustomIDs.js'
|
||||
import { Diff } from './collections/Diff.js'
|
||||
import { Diff } from './collections/Diff/index.js'
|
||||
import DisablePublish from './collections/DisablePublish.js'
|
||||
import DraftPosts from './collections/Drafts.js'
|
||||
import DraftWithMax from './collections/DraftsWithMax.js'
|
||||
@@ -14,6 +14,7 @@ import DraftsWithValidate from './collections/DraftsWithValidate.js'
|
||||
import LocalizedPosts from './collections/Localized.js'
|
||||
import { Media } from './collections/Media.js'
|
||||
import Posts from './collections/Posts.js'
|
||||
import { TextCollection } from './collections/Text.js'
|
||||
import VersionPosts from './collections/Versions.js'
|
||||
import AutosaveGlobal from './globals/Autosave.js'
|
||||
import DisablePublishGlobal from './globals/DisablePublish.js'
|
||||
@@ -42,6 +43,7 @@ export default buildConfigWithDefaults({
|
||||
VersionPosts,
|
||||
CustomIDs,
|
||||
Diff,
|
||||
TextCollection,
|
||||
Media,
|
||||
],
|
||||
globals: [AutosaveGlobal, DraftGlobal, DraftWithMaxGlobal, DisablePublishGlobal, LocalizedGlobal],
|
||||
|
||||
@@ -981,6 +981,8 @@ describe('Versions', () => {
|
||||
// Fill with invalid data again, then reload and accept the warning, should contain previous data
|
||||
await titleField.fill('')
|
||||
|
||||
await waitForAutoSaveToRunAndComplete(page, 'error')
|
||||
|
||||
await page.reload()
|
||||
|
||||
await expect(titleField).toBeEnabled()
|
||||
@@ -1197,11 +1199,11 @@ describe('Versions', () => {
|
||||
|
||||
const textInArrayES = page.locator('[data-field-path="arrayLocalized"][data-locale="es"]')
|
||||
|
||||
await expect(textInArrayES).toContainText('No Array Localizeds found')
|
||||
await expect(textInArrayES).toBeHidden()
|
||||
|
||||
await page.locator('#modifiedOnly').click()
|
||||
|
||||
await expect(textInArrayES).toBeHidden()
|
||||
await expect(textInArrayES).toContainText('No Array Localizeds found')
|
||||
})
|
||||
|
||||
test('correctly renders diff for block fields', async () => {
|
||||
@@ -1213,6 +1215,62 @@ describe('Versions', () => {
|
||||
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(3)).toHaveText('textInBlock2')
|
||||
})
|
||||
|
||||
test('correctly renders diff for collapsibles within block fields', async () => {
|
||||
await navigateToVersionFieldsDiff()
|
||||
|
||||
const textInBlock = page.locator(
|
||||
'[data-field-path="blocks.1.textInCollapsibleInCollapsibleBlock"]',
|
||||
)
|
||||
|
||||
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
|
||||
'textInCollapsibleInCollapsibleBlock',
|
||||
)
|
||||
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
|
||||
'textInCollapsibleInCollapsibleBlock2',
|
||||
)
|
||||
})
|
||||
|
||||
test('correctly renders diff for rows within block fields', async () => {
|
||||
await navigateToVersionFieldsDiff()
|
||||
|
||||
const textInBlock = page.locator('[data-field-path="blocks.1.textInRowInCollapsibleBlock"]')
|
||||
|
||||
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
|
||||
'textInRowInCollapsibleBlock',
|
||||
)
|
||||
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
|
||||
'textInRowInCollapsibleBlock2',
|
||||
)
|
||||
})
|
||||
|
||||
test('correctly renders diff for named tabs within block fields', async () => {
|
||||
await navigateToVersionFieldsDiff()
|
||||
|
||||
const textInBlock = page.locator(
|
||||
'[data-field-path="blocks.2.namedTab1InBlock.textInNamedTab1InBlock"]',
|
||||
)
|
||||
|
||||
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
|
||||
'textInNamedTab1InBlock',
|
||||
)
|
||||
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
|
||||
'textInNamedTab1InBlock2',
|
||||
)
|
||||
})
|
||||
|
||||
test('correctly renders diff for unnamed tabs within block fields', async () => {
|
||||
await navigateToVersionFieldsDiff()
|
||||
|
||||
const textInBlock = page.locator('[data-field-path="blocks.2.textInUnnamedTab2InBlock"]')
|
||||
|
||||
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(1)).toHaveText(
|
||||
'textInUnnamedTab2InBlock',
|
||||
)
|
||||
await expect(textInBlock.locator('tr').nth(1).locator('td').nth(3)).toHaveText(
|
||||
'textInUnnamedTab2InBlock2',
|
||||
)
|
||||
})
|
||||
|
||||
test('correctly renders diff for checkbox fields', async () => {
|
||||
await navigateToVersionFieldsDiff()
|
||||
|
||||
@@ -1326,12 +1384,17 @@ describe('Versions', () => {
|
||||
|
||||
const richtext = page.locator('[data-field-path="richtext"]')
|
||||
|
||||
await expect(richtext.locator('tr').nth(16).locator('td').nth(1)).toHaveText(
|
||||
'"text": "richtext",',
|
||||
)
|
||||
await expect(richtext.locator('tr').nth(16).locator('td').nth(3)).toHaveText(
|
||||
'"text": "richtext2",',
|
||||
)
|
||||
const oldDiff = richtext.locator('.lexical-diff__diff-old')
|
||||
const newDiff = richtext.locator('.lexical-diff__diff-new')
|
||||
|
||||
const oldHTML =
|
||||
`Fugiat <span data-match-type="delete">essein</span> dolor aleiqua <span data-match-type="delete">cillum</span> proident ad cillum excepteur mollit reprehenderit mollit commodo. Pariatur incididunt non exercitation est mollit nisi <span data-match-type="delete">laboredeleteofficia</span> cupidatat amet commodo commodo proident occaecat.
|
||||
`.trim()
|
||||
const newHTML =
|
||||
`Fugiat <span data-match-type="create">esse new in</span> dolor aleiqua <span data-match-type="create">gillum</span> proident ad cillum excepteur mollit reprehenderit mollit commodo. Pariatur incididunt non exercitation est mollit nisi <span data-match-type="create">labore officia</span> cupidatat amet commodo commodo proident occaecat.`.trim()
|
||||
|
||||
expect(await oldDiff.locator('p').first().innerHTML()).toEqual(oldHTML)
|
||||
expect(await newDiff.locator('p').first().innerHTML()).toEqual(newHTML)
|
||||
})
|
||||
|
||||
test('correctly renders diff for richtext fields with custom Diff component', async () => {
|
||||
|
||||
@@ -54,6 +54,7 @@ export type SupportedTimezones =
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
@@ -77,6 +78,7 @@ export interface Config {
|
||||
'version-posts': VersionPost;
|
||||
'custom-ids': CustomId;
|
||||
diff: Diff;
|
||||
text: Text;
|
||||
media: Media;
|
||||
users: User;
|
||||
'payload-jobs': PayloadJob;
|
||||
@@ -97,6 +99,7 @@ export interface Config {
|
||||
'version-posts': VersionPostsSelect<false> | VersionPostsSelect<true>;
|
||||
'custom-ids': CustomIdsSelect<false> | CustomIdsSelect<true>;
|
||||
diff: DiffSelect<false> | DiffSelect<true>;
|
||||
text: TextSelect<false> | TextSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
|
||||
@@ -312,12 +315,30 @@ export interface Diff {
|
||||
}[]
|
||||
| null;
|
||||
blocks?:
|
||||
| {
|
||||
textInBlock?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'TextBlock';
|
||||
}[]
|
||||
| (
|
||||
| {
|
||||
textInBlock?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'TextBlock';
|
||||
}
|
||||
| {
|
||||
textInCollapsibleInCollapsibleBlock?: string | null;
|
||||
textInRowInCollapsibleBlock?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'CollapsibleBlock';
|
||||
}
|
||||
| {
|
||||
namedTab1InBlock?: {
|
||||
textInNamedTab1InBlock?: string | null;
|
||||
};
|
||||
textInUnnamedTab2InBlock?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'TabsBlock';
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
checkbox?: boolean | null;
|
||||
code?: string | null;
|
||||
@@ -395,6 +416,16 @@ export interface Media {
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "text".
|
||||
*/
|
||||
export interface Text {
|
||||
id: string;
|
||||
text: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
@@ -555,6 +586,10 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'diff';
|
||||
value: string | Diff;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'text';
|
||||
value: string | Text;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'media';
|
||||
value: string | Media;
|
||||
@@ -772,6 +807,26 @@ export interface DiffSelect<T extends boolean = true> {
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
CollapsibleBlock?:
|
||||
| T
|
||||
| {
|
||||
textInCollapsibleInCollapsibleBlock?: T;
|
||||
textInRowInCollapsibleBlock?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
TabsBlock?:
|
||||
| T
|
||||
| {
|
||||
namedTab1InBlock?:
|
||||
| T
|
||||
| {
|
||||
textInNamedTab1InBlock?: T;
|
||||
};
|
||||
textInUnnamedTab2InBlock?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
};
|
||||
checkbox?: T;
|
||||
code?: T;
|
||||
@@ -803,6 +858,15 @@ export interface DiffSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "text_select".
|
||||
*/
|
||||
export interface TextSelect<T extends boolean = true> {
|
||||
text?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media_select".
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { DraftPost } from './payload-types.js'
|
||||
|
||||
import { devUser } from '../credentials.js'
|
||||
import { executePromises } from '../helpers/executePromises.js'
|
||||
import { generateLexicalData } from './collections/Diff/generateLexicalData.js'
|
||||
import {
|
||||
autosaveWithValidateCollectionSlug,
|
||||
diffCollectionSlug,
|
||||
@@ -119,6 +120,20 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
|
||||
},
|
||||
})
|
||||
|
||||
const { id: doc1ID } = await _payload.create({
|
||||
collection: 'text',
|
||||
data: {
|
||||
text: 'Document 1',
|
||||
},
|
||||
})
|
||||
|
||||
const { id: doc2ID } = await _payload.create({
|
||||
collection: 'text',
|
||||
data: {
|
||||
text: 'Document 2',
|
||||
},
|
||||
})
|
||||
|
||||
const diffDoc = await _payload.create({
|
||||
collection: diffCollectionSlug,
|
||||
locale: 'en',
|
||||
@@ -138,6 +153,18 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
|
||||
blockType: 'TextBlock',
|
||||
textInBlock: 'textInBlock',
|
||||
},
|
||||
{
|
||||
blockType: 'CollapsibleBlock',
|
||||
textInCollapsibleInCollapsibleBlock: 'textInCollapsibleInCollapsibleBlock',
|
||||
textInRowInCollapsibleBlock: 'textInRowInCollapsibleBlock',
|
||||
},
|
||||
{
|
||||
blockType: 'TabsBlock',
|
||||
namedTab1InBlock: {
|
||||
textInNamedTab1InBlock: 'textInNamedTab1InBlock',
|
||||
},
|
||||
textInUnnamedTab2InBlock: 'textInUnnamedTab2InBlock',
|
||||
},
|
||||
],
|
||||
checkbox: true,
|
||||
code: 'code',
|
||||
@@ -153,7 +180,11 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
|
||||
point: [1, 2],
|
||||
radio: 'option1',
|
||||
relationship: manyDraftsID,
|
||||
richtext: textToLexicalJSON({ text: 'richtext' }),
|
||||
richtext: generateLexicalData({
|
||||
mediaID: uploadedImage,
|
||||
textID: doc1ID,
|
||||
updated: false,
|
||||
}) as any,
|
||||
richtextWithCustomDiff: textToLexicalJSON({ text: 'richtextWithCustomDiff' }),
|
||||
select: 'option1',
|
||||
text: 'text',
|
||||
@@ -186,6 +217,18 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
|
||||
blockType: 'TextBlock',
|
||||
textInBlock: 'textInBlock2',
|
||||
},
|
||||
{
|
||||
blockType: 'CollapsibleBlock',
|
||||
textInCollapsibleInCollapsibleBlock: 'textInCollapsibleInCollapsibleBlock2',
|
||||
textInRowInCollapsibleBlock: 'textInRowInCollapsibleBlock2',
|
||||
},
|
||||
{
|
||||
blockType: 'TabsBlock',
|
||||
namedTab1InBlock: {
|
||||
textInNamedTab1InBlock: 'textInNamedTab1InBlock2',
|
||||
},
|
||||
textInUnnamedTab2InBlock: 'textInUnnamedTab2InBlock2',
|
||||
},
|
||||
],
|
||||
checkbox: false,
|
||||
code: 'code2',
|
||||
@@ -201,7 +244,11 @@ export async function seed(_payload: Payload, parallel: boolean = false) {
|
||||
point: [1, 3],
|
||||
radio: 'option2',
|
||||
relationship: draft2.id,
|
||||
richtext: textToLexicalJSON({ text: 'richtext2' }),
|
||||
richtext: generateLexicalData({
|
||||
mediaID: uploadedImage2,
|
||||
textID: doc2ID,
|
||||
updated: true,
|
||||
}) as any,
|
||||
richtextWithCustomDiff: textToLexicalJSON({ text: 'richtextWithCustomDiff2' }),
|
||||
select: 'option2',
|
||||
text: 'text2',
|
||||
|
||||
@@ -20,6 +20,8 @@ export const disablePublishSlug = 'disable-publish'
|
||||
|
||||
export const disablePublishGlobalSlug = 'disable-publish-global'
|
||||
|
||||
export const textCollectionSlug = 'text'
|
||||
|
||||
export const collectionSlugs = [
|
||||
autosaveCollectionSlug,
|
||||
draftCollectionSlug,
|
||||
@@ -27,6 +29,7 @@ export const collectionSlugs = [
|
||||
diffCollectionSlug,
|
||||
mediaCollectionSlug,
|
||||
versionCollectionSlug,
|
||||
textCollectionSlug,
|
||||
]
|
||||
|
||||
export const autoSaveGlobalSlug = 'autosave-global'
|
||||
|
||||
Reference in New Issue
Block a user