Compare commits

...

45 Commits

Author SHA1 Message Date
Elliot DeNolf
e1d9accb27 chore(release): db-mongodb/1.4.0 [skip ci] 2024-01-26 13:57:23 -05:00
Elliot DeNolf
f2f55a84cc chore(release): payload/2.9.0 [skip ci] 2024-01-26 13:55:20 -05:00
Dan Ribbens
eba53ba60a feat: forceAcceptWarning migration arg added to accept prompts (#4874)
* chore: gitignore test migrations

* feat: `forceAcceptWarning` migration args added to accept prompts

* chore: migrationDir env variable fallback

* chore: migrationDir testSuiteDir fallback

* chore: migrationDir testSuiteDir fallback fix

* chore: skip migrate down test
2024-01-26 13:48:53 -05:00
Dan Ribbens
f73d503fec fix(plugin-cloud-storage): slow get file performance large collections (#4927) 2024-01-26 13:43:55 -05:00
Dan Ribbens
6930c4e9f2 fix: upload input drawer does not show draft versions (#4903)
* chore: add field classname to upload field

* fix: upload input drawer does not show draft versions
2024-01-26 13:42:32 -05:00
Dan Ribbens
3eb681e847 fix: afterLogin hook write conflicts (#4904)
* fix: afterLogin hook conflict

* test: afterLogin hook returns for assertion

* chore: commit increment login attempt
2024-01-26 13:39:45 -05:00
Jarrod Flesch
cb4638cfa1 chore: make default views callable (#4928) 2024-01-26 13:38:36 -05:00
Dan Ribbens
b40e9f85a2 chore: use transactions in tests running mongoDB memory server (#4750)
* chore: use transactions in tests running mongoDB memory server

* chore: relationship test async setup changes

* chore: async test fix

* chore: flaky e2e localization test
2024-01-23 19:18:55 -05:00
Dan Ribbens
e5a7907a72 fix: remove No Options dropdown from hasMany fields (#4899) 2024-01-23 10:00:16 -05:00
Jarrod Flesch
3f25d1ca84 chore: re-orders request language detection (#4890) 2024-01-22 11:53:01 -05:00
Timothy Choi
d5720bea7b chore: add fieldIsGroupType type guard helper (#4872) 2024-01-19 14:19:35 -05:00
Jesse Sivonen
8ce15c8b07 fix(db-postgres): query unset relation (#4862) 2024-01-19 13:35:58 -05:00
Timothy Choi
9f5efef78f chore: lint #4766 (#4801)
* fix: import location for config test

* fix: linting fix
2024-01-18 09:25:52 -05:00
Dan Ribbens
dfba5222f3 fix(db-postgres): migrate down error (#4861) 2024-01-17 13:55:57 -05:00
Dan Ribbens
b99d24fcfa fix: migrate down missing filter for latest batch (#4860) 2024-01-17 13:54:30 -05:00
Elliot DeNolf
836ed77568 chore: update changelog [skip ci] 2024-01-16 16:39:42 -05:00
Elliot DeNolf
1c5d5b07c8 chore(release): plugin-seo/2.2.0 [skip ci] 2024-01-16 16:33:53 -05:00
Elliot DeNolf
da5f1f2240 chore(release): plugin-form-builder/1.2.0 [skip ci] 2024-01-16 16:33:44 -05:00
Elliot DeNolf
c84c58c7b4 chore(release): db-postgres/0.4.0 [skip ci] 2024-01-16 16:33:21 -05:00
Elliot DeNolf
1c1b8f3cec chore(release): db-mongodb/1.3.2 [skip ci] 2024-01-16 16:32:52 -05:00
Elliot DeNolf
3f69f83180 chore(release): payload/2.8.2 [skip ci] 2024-01-16 16:31:39 -05:00
beezee
371353f153 feat(db-postgres): support drizzle logging config (#4809) 2024-01-16 15:45:17 -05:00
Dan Ribbens
a92c6334b6 chore(db-postgres): update drizzle-kit to 0.20.5-608ae62 and drizzle-orm to latest (#4772) 2024-01-16 13:35:07 -05:00
Paul
eb9e771a9c fix(db-postgres): Remove duplicate keys from response (#4747)
* Remove duplicate keys from response

* Update to delete duplicates at a higher level and remove '_order' from array rows too
2024-01-16 13:22:08 -05:00
Patrik
ee5390aaca fix: removes max-width from field-types class & correctly sets it on uploads (#4829) 2024-01-16 13:12:58 -05:00
Paul
a861311c5a fix(db-mongodb): mongodb versions creating duplicates (#4825)
* Fixes the issue with mongodb versions

* Update other methods to use options too
2024-01-16 12:20:45 -05:00
James Mikrut
74c3fe1bb2 Merge pull request #4806 from payloadcms/fix/#4775-postgres-block-validation-arrays
fix(db-postgres): validateExistingBlockIsIdentical with arrays
2024-01-15 15:04:47 -05:00
James Mikrut
a2be50279e Merge pull request #4804 from payloadcms/fix/#4802-transaction-options-false
fix(db-mongodb): transactionOptions=false typeErrors
2024-01-15 15:04:01 -05:00
James Mikrut
403eb06acf Merge pull request #4723 from payloadcms/fix/4548-fix-missing-spread
fix(plugin-seo):Fix missing spread operator in URL generator function
2024-01-15 15:02:45 -05:00
James Mikrut
f5c2cd74cc Merge pull request #4695 from payloadcms/feat/4539-seo-plugin-allow-field-and-interface-overrides
feat(plugin-seo): Add support for interfaceName and fieldOverrides
2024-01-15 15:02:14 -05:00
Paul Popus
a6a1963ec6 Merge branch 'main' into feat/4539-seo-plugin-allow-field-and-interface-overrides 2024-01-15 16:29:49 -03:00
Dan Ribbens
0647c870f1 fix(db-postgres): validateExistingBlockIsIdentical with other tables 2024-01-13 22:46:05 -05:00
Dan Ribbens
3b88adc7d0 fix(db-postgres): validateExistingBlockIsIdentical with arrays 2024-01-13 22:40:30 -05:00
Dan Ribbens
82383a5b5f fix(db-mongodb): transactionOptions=false typeErrors 2024-01-13 14:59:16 -05:00
James Mikrut
f9dda628b2 Merge pull request #4730 from payloadcms/feat/4471-add-validation-for-form-submission
feat(plugin-form-builder):Add validation for form ID when creating a form submissions
2024-01-12 15:39:35 -05:00
Elliot DeNolf
93eb0e4a31 chore: update bug report template to renamed possible-bug label 2024-01-12 14:19:43 -05:00
Elliot DeNolf
2e362f44f4 chore(release): payload/2.8.1 [skip ci] 2024-01-12 12:44:15 -05:00
Jarrod Flesch
775502b161 fix: corrects config usage in build bin script (#4796) 2024-01-12 12:40:08 -05:00
Elliot DeNolf
84d75ce6ca chore(release): plugin-form-builder/1.1.2 [skip ci] 2024-01-12 10:47:08 -05:00
Elliot DeNolf
175cf229c0 chore(release): richtext-lexical/0.5.2 [skip ci] 2024-01-12 10:41:55 -05:00
Paul Popus
2b731c1088 feat(plugin-form-builder):Add validation for form ID when creating a submission 2024-01-08 20:35:52 -03:00
Paul Popus
6affa1c304 Removed fallback for interfaceName 2024-01-08 15:27:17 -03:00
Paul Popus
57dc93da5d Fix missing spread operator in generator function 2024-01-08 14:07:35 -03:00
Paul Popus
28d3f73c2a Fix integration test 2024-01-05 15:23:57 -03:00
Paul Popus
7eae86bcb3 Add changes from previous branch and update docs 2024-01-05 12:45:03 -03:00
90 changed files with 1689 additions and 547 deletions

View File

@@ -1,6 +1,6 @@
name: Bug Report
description: Create a bug report for Payload
labels: ['possible-bug']
labels: ['[possible-bug]']
body:
- type: markdown
attributes:

View File

@@ -1,3 +1,46 @@
## [2.9.0](https://github.com/payloadcms/payload/compare/v2.8.2...v2.9.0) (2024-01-26)
### Features
* forceAcceptWarning migration arg added to accept prompts ([#4874](https://github.com/payloadcms/payload/issues/4874)) ([eba53ba](https://github.com/payloadcms/payload/commit/eba53ba60afd7c5d37389377ed06a9b556058d49))
### Bug Fixes
* afterLogin hook write conflicts ([#4904](https://github.com/payloadcms/payload/issues/4904)) ([3eb681e](https://github.com/payloadcms/payload/commit/3eb681e847e9c55eaaa69c22bea4f4e66c7eac36))
* **db-postgres:** migrate down error ([#4861](https://github.com/payloadcms/payload/issues/4861)) ([dfba522](https://github.com/payloadcms/payload/commit/dfba5222f3abf3f236dc9212a28e1aec7d7214d5))
* **db-postgres:** query unset relation ([#4862](https://github.com/payloadcms/payload/issues/4862)) ([8ce15c8](https://github.com/payloadcms/payload/commit/8ce15c8b07800397a50dcf790c263ed5b3cfad53))
* migrate down missing filter for latest batch ([#4860](https://github.com/payloadcms/payload/issues/4860)) ([b99d24f](https://github.com/payloadcms/payload/commit/b99d24fcfa698c493ea01c41621201abe18fabe3))
* **plugin-cloud-storage:** slow get file performance large collections ([#4927](https://github.com/payloadcms/payload/issues/4927)) ([f73d503](https://github.com/payloadcms/payload/commit/f73d503fecdfa5cefdc26ab9aad60b00563f881e))
* remove No Options dropdown from hasMany fields ([#4899](https://github.com/payloadcms/payload/issues/4899)) ([e5a7907](https://github.com/payloadcms/payload/commit/e5a7907a72c1371447ac2f71fce213ed22246092))
* upload input drawer does not show draft versions ([#4903](https://github.com/payloadcms/payload/issues/4903)) ([6930c4e](https://github.com/payloadcms/payload/commit/6930c4e9f2200853121391ad8f8df48ea66c40a4))
## [2.8.2](https://github.com/payloadcms/payload/compare/v2.8.1...v2.8.2) (2024-01-16)
### Features
* **db-postgres:** support drizzle logging config ([#4809](https://github.com/payloadcms/payload/issues/4809)) ([371353f](https://github.com/payloadcms/payload/commit/371353f1535fbab4ebd9f56fc14fd10a30eec289))
* **plugin-form-builder:** add validation for form ID when creating a submission ([#4730](https://github.com/payloadcms/payload/pull/4730))
* **plugin-seo:** add support for interfaceName and fieldOverrides ([#4695](https://github.com/payloadcms/payload/pull/4695))
### Bug Fixes
* **db-mongodb:** mongodb versions creating duplicates ([#4825](https://github.com/payloadcms/payload/issues/4825)) ([a861311](https://github.com/payloadcms/payload/commit/a861311c5a98126700f98f9a2ab380782e754717))
* **db-mongodb:** transactionOptions=false typeErrors ([82383a5](https://github.com/payloadcms/payload/commit/82383a5b5f52785115c0feb970da70e91971b7ca))
* **db-postgres:** Remove duplicate keys from response ([#4747](https://github.com/payloadcms/payload/issues/4747)) ([eb9e771](https://github.com/payloadcms/payload/commit/eb9e771a9ca03636486d36654f215b73435574cb))
* **db-postgres:** validateExistingBlockIsIdentical with arrays ([3b88adc](https://github.com/payloadcms/payload/commit/3b88adc7d0594af63ce190c40c9ee3905df67a31))
* **db-postgres:** validateExistingBlockIsIdentical with other tables ([0647c87](https://github.com/payloadcms/payload/commit/0647c870f15dc1b122734b678c2abeb6f56377d4))
* **plugin-seo:** fix missing spread operator in URL generator function ([#4723](https://github.com/payloadcms/payload/pull/4723))
* removes max-width from field-types class & correctly sets it on uploads ([#4829](https://github.com/payloadcms/payload/issues/4829)) ([ee5390a](https://github.com/payloadcms/payload/commit/ee5390aaca37a4154cde8392b60f091ec3e5175c))
## [2.8.1](https://github.com/payloadcms/payload/compare/v2.8.0...v2.8.1) (2024-01-12)
### Bug Fixes
* corrects config usage in build bin script ([#4796](https://github.com/payloadcms/payload/issues/4796)) ([775502b](https://github.com/payloadcms/payload/commit/775502b1616c1bd35a3044438e253a0e84219f99))
## [2.8.0](https://github.com/payloadcms/payload/compare/v2.7.0...v2.8.0) (2024-01-12)

View File

@@ -159,6 +159,39 @@ A function called by the search preview component to display the actual URL of y
}
```
#### `interfaceName`
Rename the meta group interface name that is generated for TypeScript and GraphQL.
```ts
// payload.config.ts
{
// ...
seoPlugin({
interfaceName: 'customInterfaceNameSEO'
})
}
```
#### `fieldOverrides`
Pass any valid field props to the base fields: Title, Description or Image.
```ts
// payload.config.ts
seoPlugin({
// ...
fieldOverrides: {
title: {
required: true,
},
description: {
localized: true,
},
},
})
```
## TypeScript
All types can be directly imported:

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "1.3.1",
"version": "1.4.0",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -29,15 +29,18 @@ export const connect: Connect = async function connect(this: MongooseAdapter, pa
urlToConnect = process.env.PAYLOAD_TEST_MONGO_URL
} else {
connectionOptions.dbName = 'payloadmemory'
const { MongoMemoryServer } = require('mongodb-memory-server')
const { MongoMemoryReplSet } = require('mongodb-memory-server')
const getPort = require('get-port')
const port = await getPort()
this.mongoMemoryServer = await MongoMemoryServer.create({
this.mongoMemoryServer = await MongoMemoryReplSet.create({
instance: {
dbName: 'payloadmemory',
port,
},
replSet: {
count: 3,
},
})
urlToConnect = this.mongoMemoryServer.getUri()
@@ -50,7 +53,7 @@ export const connect: Connect = async function connect(this: MongooseAdapter, pa
const client = this.connection.getClient()
if (!client.options.replicaSet || this.transactionOptions === false) {
if (!client.options.replicaSet) {
this.transactionOptions = false
this.beginTransaction = undefined
}

View File

@@ -49,6 +49,7 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
],
},
{ $unset: { latest: 1 } },
options,
)
const result: Document = JSON.parse(JSON.stringify(doc))

View File

@@ -57,6 +57,7 @@ export const createVersion: CreateVersion = async function createVersion(
],
},
{ $unset: { latest: 1 } },
options,
)
const result: Document = JSON.parse(JSON.stringify(doc))

View File

@@ -63,6 +63,7 @@ export const find: Find = async function find(
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
Model.countDocuments(query, {
...options,
hint: { _id: 1 },
}),
)

View File

@@ -82,6 +82,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
Model.countDocuments(query, {
...options,
hint: { _id: 1 },
}),
)

View File

@@ -79,6 +79,7 @@ export const findVersions: FindVersions = async function findVersions(
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
Model.countDocuments(query, {
...options,
hint: { _id: 1 },
}),
)

View File

@@ -93,18 +93,13 @@ export function mongooseAdapter({
connectOptions,
disableIndexHints = false,
migrationDir: migrationDirArg,
transactionOptions,
transactionOptions = {},
url,
}: Args): MongooseAdapterResult {
function adapter({ payload }: { payload: Payload }) {
const migrationDir = findMigrationDir(migrationDirArg)
let beginTransactionFunction = beginTransaction
mongoose.set('strictQuery', false)
if (transactionOptions === false) {
beginTransactionFunction = () => null
}
return createDatabaseAdapter<MongooseAdapter>({
name: 'mongoose',
@@ -122,7 +117,7 @@ export function mongooseAdapter({
versions: {},
// DatabaseAdapter
beginTransaction: beginTransactionFunction,
beginTransaction: transactionOptions ? beginTransaction : undefined,
commitTransaction,
connect,
create,

View File

@@ -11,9 +11,13 @@ import type { MongooseAdapter } from '.'
/**
* Drop the current database and run all migrate up functions
*/
export async function migrateFresh(this: MongooseAdapter): Promise<void> {
export async function migrateFresh(
this: MongooseAdapter,
{ forceAcceptWarning = false }: { forceAcceptWarning?: boolean },
): Promise<void> {
const { payload } = this
if (!forceAcceptWarning) {
const { confirm: acceptWarning } = await prompts(
{
name: 'confirm',
@@ -31,6 +35,7 @@ export async function migrateFresh(this: MongooseAdapter): Promise<void> {
if (!acceptWarning) {
process.exit(0)
}
}
payload.logger.info({
msg: `Dropping database.`,

View File

@@ -77,6 +77,7 @@ export const sanitizeQueryValue = ({
// Object equality requires the value to be the first key in the object that is being queried.
if (
operator === 'equals' &&
formattedValue &&
typeof formattedValue === 'object' &&
formattedValue.value &&
formattedValue.relationTo

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "0.3.1",
"version": "0.4.0",
"description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -22,8 +22,8 @@
"dependencies": {
"@libsql/client": "^0.3.1",
"console-table-printer": "2.11.2",
"drizzle-kit": "0.19.13-e99bac1",
"drizzle-orm": "0.28.5",
"drizzle-kit": "0.20.5-608ae62",
"drizzle-orm": "0.29.3",
"pg": "8.11.3",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",

View File

@@ -18,8 +18,9 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
try {
this.pool = new Pool(this.poolOptions)
await this.pool.connect()
const logger = this.logger || false
this.drizzle = drizzle(this.pool, { schema: this.schema })
this.drizzle = drizzle(this.pool, { schema: this.schema, logger })
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
this.payload.logger.info('---- DROPPING TABLES ----')
await this.drizzle.execute(sql`drop schema public cascade;
@@ -39,7 +40,7 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
)
return
const { pushSchema } = require('drizzle-kit/utils')
const { pushSchema } = require('drizzle-kit/payload')
// This will prompt if clarifications are needed for Drizzle to push new schema
const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema(
@@ -59,9 +60,9 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
const { confirm: acceptWarnings } = await prompts(
{
name: 'confirm',
type: 'confirm',
initial: false,
message,
type: 'confirm',
},
{
onCancel: () => {

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-restricted-syntax, no-await-in-loop */
import type { DrizzleSnapshotJSON } from 'drizzle-kit/utils'
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
import type { CreateMigration } from 'payload/database'
import fs from 'fs'
@@ -53,14 +53,14 @@ const getDefaultDrizzleSnapshot = (): DrizzleSnapshotJSON => ({
export const createMigration: CreateMigration = async function createMigration(
this: PostgresAdapter,
{ migrationName, payload },
{ forceAcceptWarning, migrationName, payload },
) {
const dir = payload.db.migrationDir
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/utils')
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/payload')
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
const formattedDate = yyymmdd.replace(/\D/g, '')
@@ -95,13 +95,13 @@ export const createMigration: CreateMigration = async function createMigration(
const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
const sqlStatementsDown = await generateMigration(drizzleJsonAfter, drizzleJsonBefore)
if (!sqlStatementsUp.length && !sqlStatementsDown.length) {
if (!sqlStatementsUp.length && !sqlStatementsDown.length && !forceAcceptWarning) {
const { confirm: shouldCreateBlankMigration } = await prompts(
{
name: 'confirm',
type: 'confirm',
initial: false,
message: 'No schema changes detected. Would you like to create a blank migration file?',
type: 'confirm',
},
{
onCancel: () => {

View File

@@ -50,6 +50,7 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
drizzle: undefined,
enums: {},
fieldConstraints: {},
logger: args.logger,
pool: undefined,
poolOptions: args.pool,
push: args.push,

View File

@@ -80,7 +80,7 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
}
async function runMigrationFile(payload: Payload, migration: Migration, batch: number) {
const { generateDrizzleJson } = require('drizzle-kit/utils')
const { generateDrizzleJson } = require('drizzle-kit/payload')
const start = Date.now()
const req = { payload } as PayloadRequest

View File

@@ -37,7 +37,7 @@ export async function migrateDown(this: PostgresAdapter): Promise<void> {
}
const start = Date.now()
const req = {} as PayloadRequest
const req = { payload } as PayloadRequest
try {
payload.logger.info({ msg: `Migrating down: ${migrationFile.name}` })

View File

@@ -14,9 +14,13 @@ import { parseError } from './utilities/parseError'
/**
* Drop the current database and run all migrate up functions
*/
export async function migrateFresh(this: PostgresAdapter): Promise<void> {
export async function migrateFresh(
this: PostgresAdapter,
{ forceAcceptWarning = false },
): Promise<void> {
const { payload } = this
if (forceAcceptWarning === false) {
const { confirm: acceptWarning } = await prompts(
{
name: 'confirm',
@@ -34,6 +38,7 @@ export async function migrateFresh(this: PostgresAdapter): Promise<void> {
if (!acceptWarning) {
process.exit(0)
}
}
payload.logger.info({
msg: `Dropping database.`,

View File

@@ -2,7 +2,7 @@
import type { SQL } from 'drizzle-orm'
import type { Field, FieldAffectingData, TabAsField } from 'payload/types'
import { and, eq, sql } from 'drizzle-orm'
import { and, eq, like, sql } from 'drizzle-orm'
import { alias } from 'drizzle-orm/pg-core'
import { APIError } from 'payload/errors'
import { fieldAffectsData, tabHasName } from 'payload/types'
@@ -317,21 +317,15 @@ export const getTableColumnFromPath = ({
// Join in the relationships table
joinAliases.push({
condition: eq(
(aliasTable || adapter.tables[rootTableName]).id,
aliasRelationshipTable.parent,
condition: and(
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
),
table: aliasRelationshipTable,
})
selectFields[`${relationTableName}.path`] = aliasRelationshipTable.path
constraints.push({
columnName: 'path',
table: aliasRelationshipTable,
value: `${constraintPath}${field.name}`,
})
let newAliasTable
if (typeof field.relationTo === 'string') {
@@ -428,7 +422,7 @@ export const getTableColumnFromPath = ({
columnName: `${columnPrefix}${field.name}`,
constraints,
field,
pathSegments: pathSegments,
pathSegments,
table: targetTable,
}
}

View File

@@ -207,6 +207,16 @@ export async function parseParams({
break
}
if (operator === 'equals' && queryValue === null) {
constraints.push(isNull(rawColumn || table[columnName]))
break
}
if (operator === 'not_equals' && queryValue === null) {
constraints.push(isNotNull(rawColumn || table[columnName]))
break
}
constraints.push(
operatorMap[queryOperator](rawColumn || table[columnName], queryValue),
)

View File

@@ -1,46 +0,0 @@
// type GenerateMigration = (before: DrizzleSnapshotJSON, after: DrizzleSnapshotJSON) => string[]
// type GenerateDrizzleJSON = (schema: DrizzleSchemaExports) => DrizzleSnapshotJSON
// type PushDiff = (schema: DrizzleSchemaExports) => Promise<{ warnings: string[], apply: () => Promise<void> }>
// drizzle-kit@utils
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
async function generateUsage() {
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/utils')
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const schema = await import('./data/users')
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const schemaAfter = await import('./data/users-after')
const drizzleJsonBefore = generateDrizzleJson(schema)
const drizzleJsonAfter = generateDrizzleJson(schemaAfter)
const sqlStatements = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
console.log(sqlStatements)
}
async function pushUsage() {
const { pushSchema } = require('drizzle-kit/utils')
// @ts-expect-error Just TypeScript being broken // TODO: Open TypeScript issue
const schemaAfter = await import('./data/users-after')
const db = drizzle(new Pool({ connectionString: '' }))
const response = await pushSchema(schemaAfter, db)
console.log('\n')
console.log('hasDataLoss: ', response.hasDataLoss)
console.log('warnings: ', response.warnings)
console.log('statements: ', response.statementsToExecute)
await response.apply()
process.exit(0)
}

View File

@@ -16,7 +16,10 @@ const getFlattenedFieldNames = (fields: Field[], prefix: string = ''): string[]
return fields.reduce((fieldsToUse, field) => {
let fieldPrefix = prefix
if (field.type === 'blocks') {
if (
['array', 'blocks', 'relationship', 'upload'].includes(field.type) ||
('hasMany' in field && field.hasMany === true)
) {
return fieldsToUse
}
@@ -54,7 +57,6 @@ export const validateExistingBlockIsIdentical = ({
rootTableName,
table,
}: Args): void => {
if (table) {
const fieldNames = getFlattenedFieldNames(block.fields)
const missingField =
@@ -78,5 +80,4 @@ export const validateExistingBlockIsIdentical = ({
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One is localized, but another is not. Block schemas of the same name must match exactly.`,
)
}
}
}

View File

@@ -7,8 +7,8 @@ import { fieldAffectsData } from 'payload/types'
import type { BlocksMap } from '../../utilities/createBlocksMap'
import { transformHasManyNumber } from './hasManyNumber'
import { transformRelationship } from './relationship'
import { transformHasManyText } from './hasManyText'
import { transformRelationship } from './relationship'
type TraverseFieldsArgs = {
/**
@@ -35,10 +35,6 @@ type TraverseFieldsArgs = {
* An array of Payload fields to traverse
*/
fields: (Field | TabAsField)[]
/**
* All hasMany text fields, as returned by Drizzle, keyed on an object by field path
*/
texts: Record<string, Record<string, unknown>[]>
/**
* All hasMany number fields, as returned by Drizzle, keyed on an object by field path
*/
@@ -55,6 +51,10 @@ type TraverseFieldsArgs = {
* Data structure representing the nearest table from db
*/
table: Record<string, unknown>
/**
* All hasMany text fields, as returned by Drizzle, keyed on an object by field path
*/
texts: Record<string, Record<string, unknown>[]>
}
// Traverse fields recursively, transforming data
@@ -66,11 +66,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix,
fields,
texts,
numbers,
path,
relationships,
table,
texts,
}: TraverseFieldsArgs): T => {
const sanitizedPath = path ? `${path}.` : path
@@ -83,11 +83,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
texts,
numbers,
path,
relationships,
table,
texts,
})
}
@@ -103,17 +103,22 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix,
fields: field.fields,
texts,
numbers,
path,
relationships,
table,
texts,
})
}
if (fieldAffectsData(field)) {
const fieldName = `${fieldPrefix || ''}${field.name}`
const fieldData = table[fieldName]
if (fieldPrefix) {
deletions.push(() => delete table[fieldName])
}
if (field.type === 'array') {
if (Array.isArray(fieldData)) {
if (field.localized) {
@@ -135,13 +140,17 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix: '',
fields: field.fields,
texts,
numbers,
path: `${sanitizedPath}${field.name}.${row._order - 1}`,
relationships,
table: row,
texts,
})
if ('_order' in rowResult) {
delete rowResult._order
}
arrayResult[locale].push(rowResult)
}
@@ -153,6 +162,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
row.id = row._uuid
delete row._uuid
}
if ('_order' in row) {
delete row._order
}
return traverseFields<T>({
blocks,
config,
@@ -160,11 +174,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix: '',
fields: field.fields,
texts,
numbers,
path: `${sanitizedPath}${field.name}.${i}`,
relationships,
table: row,
texts,
})
})
}
@@ -204,11 +218,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix: '',
fields: block.fields,
texts,
numbers,
path: `${blockFieldPath}.${row._order - 1}`,
relationships,
table: row,
texts,
})
delete blockResult._order
@@ -235,11 +249,11 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix: '',
fields: block.fields,
texts,
numbers,
path: `${blockFieldPath}.${i}`,
relationships,
table: row,
texts,
})
}
@@ -316,15 +330,15 @@ export const traverseFields = <T extends Record<string, unknown>>({
transformHasManyText({
field,
locale,
textRows: texts,
ref: result,
textRows: texts,
})
})
} else {
transformHasManyText({
field,
textRows: textPathMatch,
ref: result,
textRows: textPathMatch,
})
}
@@ -420,13 +434,16 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix: groupFieldPrefix,
fields: field.fields,
texts,
numbers,
path: `${sanitizedPath}${field.name}`,
relationships,
table,
texts,
})
})
if ('_order' in ref) {
delete ref._order
}
} else {
const groupData = {}
@@ -437,12 +454,15 @@ export const traverseFields = <T extends Record<string, unknown>>({
deletions,
fieldPrefix: groupFieldPrefix,
fields: field.fields,
texts,
numbers,
path: `${sanitizedPath}${field.name}`,
relationships,
table,
texts,
})
if ('_order' in ref) {
delete ref._order
}
}
break

View File

@@ -1,6 +1,7 @@
import type {
ColumnBaseConfig,
ColumnDataType,
DrizzleConfig,
ExtractTablesWithRelations,
Relation,
Relations,
@@ -9,11 +10,13 @@ import type { NodePgDatabase, NodePgQueryResultHKT } from 'drizzle-orm/node-post
import type { PgColumn, PgEnum, PgTableWithColumns, PgTransaction } from 'drizzle-orm/pg-core'
import type { Payload } from 'payload'
import type { BaseDatabaseAdapter } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import type { Pool, PoolConfig } from 'pg'
export type DrizzleDB = NodePgDatabase<Record<string, unknown>>
export type Args = {
logger?: DrizzleConfig['logger']
migrationDir?: string
pool: PoolConfig
push?: boolean
@@ -48,6 +51,12 @@ export type DrizzleTransaction = PgTransaction<
export type PostgresAdapter = BaseDatabaseAdapter & {
drizzle: DrizzleDB
enums: Record<string, GenericEnum>
/**
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
* Used for returning properly formed errors from unique fields
*/
fieldConstraints: Record<string, Record<string, string>>
logger: DrizzleConfig['logger']
pool: Pool
poolOptions: Args['pool']
push: boolean
@@ -61,17 +70,12 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
}
}
tables: Record<string, GenericTable>
/**
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
* Used for returning properly formed errors from unique fields
*/
fieldConstraints: Record<string, Record<string, string>>
}
export type PostgresAdapterResult = (args: { payload: Payload }) => PostgresAdapter
export type MigrateUpArgs = { payload: Payload }
export type MigrateDownArgs = { payload: Payload }
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequest> }
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequest> }
declare module 'payload' {
export interface DatabaseAdapter
@@ -79,6 +83,7 @@ declare module 'payload' {
BaseDatabaseAdapter {
drizzle: DrizzleDB
enums: Record<string, GenericEnum>
fieldConstraints: Record<string, Record<string, string>>
pool: Pool
push: boolean
relations: Record<string, GenericRelation>
@@ -91,6 +96,5 @@ declare module 'payload' {
}
}
tables: Record<string, GenericTable>
fieldConstraints: Record<string, Record<string, string>>
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.8.0",
"version": "2.9.0",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",

View File

@@ -20,7 +20,7 @@ export const getCustomViews = (args: {
? collection?.admin?.components?.views?.Edit
: undefined
const defaultViewKeys = Object.keys(defaultCollectionViews)
const defaultViewKeys = Object.keys(defaultCollectionViews())
customViews = Object.entries(collectionViewsConfig || {}).reduce((prev, [key, view]) => {
if (defaultViewKeys.includes(key)) {
@@ -38,7 +38,7 @@ export const getCustomViews = (args: {
? global?.admin?.components?.views?.Edit
: undefined
const defaultViewKeys = Object.keys(defaultGlobalViews)
const defaultViewKeys = Object.keys(defaultGlobalViews())
customViews = Object.entries(globalViewsConfig || {}).reduce((prev, [key, view]) => {
if (defaultViewKeys.includes(key)) {

View File

@@ -133,9 +133,10 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
const moreThanOneAvailableCollection = enabledCollectionConfigs.length > 1
useEffect(() => {
const { slug, admin: { listSearchableFields } = {} } = selectedCollectionConfig
const { slug, admin: { listSearchableFields } = {}, versions } = selectedCollectionConfig
const params: {
cacheBust?: number
draft?: string
limit?: number
page?: number
search?: string
@@ -172,6 +173,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
if (sort) params.sort = sort
if (cacheBust) params.cacheBust = cacheBust
if (copyOfWhere) params.where = copyOfWhere
if (versions?.drafts) params.draft = 'true'
setParams(params)
}, [

View File

@@ -13,7 +13,6 @@
& > .field-type {
margin-bottom: var(--spacing-field);
max-width: 100%;
&[type='hidden'] {
margin-bottom: 0;

View File

@@ -151,7 +151,7 @@ const NumberField: React.FC<Props> = (props) => {
if (isOverHasMany) {
return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
}
return t('general:noOptions')
return null
}}
numberOnly
onChange={handleHasManyChange}
@@ -170,7 +170,7 @@ const NumberField: React.FC<Props> = (props) => {
onChange={handleChange}
onWheel={(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
e.target.blur()
}}
placeholder={getTranslation(placeholder, i18n)}

View File

@@ -110,7 +110,7 @@ const TextInput: React.FC<TextInputProps> = (props) => {
if (isOverHasMany) {
return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
}
return t('general:noOptions')
return null
}}
onChange={onChange}
options={[]}

View File

@@ -137,6 +137,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
fieldBaseClass,
baseClass,
className,
`field-${path.replace(/\./g, '__')}`,
showError && 'error',
readOnly && 'read-only',
]

View File

@@ -2,6 +2,7 @@
.upload {
position: relative;
max-width: 100%;
&__wrap {
background: var(--theme-elevation-50);

View File

@@ -17,9 +17,9 @@ export type globalViewType =
| 'Version'
| 'Versions'
export const defaultGlobalViews: {
export const defaultGlobalViews = (): {
[key in globalViewType]: React.ComponentType<any>
} = {
} => ({
API,
Default: DefaultGlobalEdit,
LivePreview: LivePreviewView,
@@ -27,7 +27,7 @@ export const defaultGlobalViews: {
Relationships: null,
Version: VersionView,
Versions: VersionsView,
}
})
export const CustomGlobalComponent = (
args: GlobalEditViewProps & {
@@ -43,18 +43,14 @@ export const CustomGlobalComponent = (
// For example, the Edit view:
// 1. Edit?.Default
// 2. Edit?.Default?.Component
// TODO: Remove the `@ts-ignore` when a Typescript wizard arrives
// For some reason `Component` does not exist on type `Edit[view]` no matter how narrow the type is
const Component =
typeof Edit === 'object' && typeof Edit[view] === 'function'
? Edit[view]
: typeof Edit === 'object' &&
typeof Edit?.[view] === 'object' &&
// @ts-ignore
typeof Edit[view].Component === 'function'
? // @ts-ignore
Edit[view].Component
: defaultGlobalViews[view]
? Edit[view].Component
: defaultGlobalViews()[view]
if (Component) {
return <Component {...args} />

View File

@@ -17,9 +17,9 @@ export type collectionViewType =
| 'Version'
| 'Versions'
export const defaultCollectionViews: {
export const defaultCollectionViews = (): {
[key in collectionViewType]: React.ComponentType<any>
} = {
} => ({
API,
Default: DefaultCollectionEdit,
LivePreview: LivePreviewView,
@@ -27,7 +27,7 @@ export const defaultCollectionViews: {
Relationships: null,
Version: VersionView,
Versions: VersionsView,
}
})
export const CustomCollectionComponent = (
args: CollectionEditViewProps & {
@@ -43,18 +43,15 @@ export const CustomCollectionComponent = (
// For example, the Edit view:
// 1. Edit?.Default
// 2. Edit?.Default?.Component
// TODO: Remove the `@ts-ignore` when a Typescript wizard arrives
// For some reason `Component` does not exist on type `Edit[view]` no matter how narrow the type is
const Component =
typeof Edit === 'object' && typeof Edit[view] === 'function'
? Edit[view]
: typeof Edit === 'object' &&
typeof Edit?.[view] === 'object' &&
// @ts-ignore
typeof Edit[view].Component === 'function'
? // @ts-ignore
Edit[view].Component
: defaultCollectionViews[view]
? Edit[view].Component
: defaultCollectionViews()[view]
if (Component) {
return <Component {...args} />

View File

@@ -18,8 +18,8 @@ import sanitizeInternalFields from '../../utilities/sanitizeInternalFields'
import isLocked from '../isLocked'
import { authenticateLocalStrategy } from '../strategies/local/authenticate'
import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts'
import { resetLoginAttempts } from '../strategies/local/resetLoginAttempts'
import { getFieldsToSign } from './getFieldsToSign'
import unlock from './unlock'
export type Result = {
exp?: number
@@ -115,16 +115,16 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
})
}
if (shouldCommit) await commitTransaction(req)
throw new AuthenticationError(req.t)
}
if (maxLoginAttemptsEnabled) {
await unlock({
collection: {
config: collectionConfig,
},
data,
overrideAccess: true,
await resetLoginAttempts({
collection: collectionConfig,
doc: user,
payload: req.payload,
req,
})
}

View File

@@ -52,5 +52,6 @@ export const incrementLoginAttempts = async ({
id: doc.id,
collection: collection.slug,
data,
req,
})
}

View File

@@ -15,6 +15,7 @@ export const resetLoginAttempts = async ({
payload,
req,
}: Args): Promise<void> => {
if (!('lockUntil' in doc && typeof doc.lockUntil === 'string') || doc.loginAttempts === 0) return
await payload.update({
id: doc.id,
collection: collection.slug,
@@ -22,6 +23,7 @@ export const resetLoginAttempts = async ({
lockUntil: null,
loginAttempts: 0,
},
overrideAccess: true,
req,
})
}

View File

@@ -1,10 +1,9 @@
import payload from '..'
import loadConfig from '../config/load'
export const build = async (): Promise<void> => {
const config = await loadConfig() // Will throw its own error if it fails
await payload.config.admin.bundler.build(config)
await config.admin.bundler.build(config)
}
// when build.js is launched directly

View File

@@ -27,7 +27,7 @@ const availableCommands = [
const availableCommandsMsg = `Available commands: ${availableCommands.join(', ')}`
export const migrate = async (parsedArgs: ParsedArgs): Promise<void> => {
const { _: args, file, help } = parsedArgs
const { _: args, file, forceAcceptWarning, help } = parsedArgs
if (help) {
// eslint-disable-next-line no-console
console.log(`\n\n${availableCommandsMsg}\n`) // Avoid having to init payload to get the logger
@@ -74,11 +74,16 @@ export const migrate = async (parsedArgs: ParsedArgs): Promise<void> => {
await adapter.migrateReset()
break
case 'migrate:fresh':
await adapter.migrateFresh()
await adapter.migrateFresh({ forceAcceptWarning })
break
case 'migrate:create':
try {
await adapter.createMigration({ file, migrationName: args[1], payload })
await adapter.createMigration({
file,
forceAcceptWarning,
migrationName: args[1],
payload,
})
} catch (err) {
throw new Error(`Error creating migration: ${err.message}`)
}

View File

@@ -39,7 +39,7 @@ export function createDatabaseAdapter<T extends BaseDatabaseAdapter>(
createMigration,
migrate,
migrateDown,
migrateFresh: async () => null,
migrateFresh: async ({ forceAcceptWarning = null }) => null,
migrateRefresh,
migrateReset,
migrateStatus,

View File

@@ -25,7 +25,9 @@ export async function migrateDown(this: BaseDatabaseAdapter): Promise<void> {
msg: `Rolling back batch ${latestBatch} consisting of ${existingMigrations.length} migration(s).`,
})
for (const migration of existingMigrations) {
const latestBatchMigrations = existingMigrations.filter(({ batch }) => batch === latestBatch)
for (const migration of latestBatchMigrations) {
const migrationFile = migrationFiles.find((m) => m.name === migration.name)
if (!migrationFile) {
throw new Error(`Migration ${migration.name} not found locally.`)

View File

@@ -78,7 +78,7 @@ export interface BaseDatabaseAdapter {
/**
* Drop the current database and run all migrate up functions
*/
migrateFresh: () => Promise<void>
migrateFresh: (args: { forceAcceptWarning?: boolean }) => Promise<void>
/**
* Run all migration down functions before running up
*/
@@ -138,6 +138,10 @@ export type Destroy = (payload: Payload) => Promise<void>
export type CreateMigration = (args: {
file?: string
/**
* Skips the prompt asking to create empty migrations
*/
forceAcceptWarning?: boolean
migrationName?: string
payload: Payload
}) => Promise<void>

View File

@@ -109,6 +109,7 @@ export {
fieldHasSubFields,
fieldIsArrayType,
fieldIsBlockType,
fieldIsGroupType,
fieldIsLocalized,
fieldIsPresentationalOnly,
fieldSupportsMany,

View File

@@ -577,7 +577,7 @@ export type Block = {
*/
interfaceName?: string
labels?: Labels
slug: string,
slug: string
/** Extension point to add your custom data. */
custom?: Record<string, any>
}
@@ -692,6 +692,10 @@ export function fieldIsBlockType(field: Field): field is BlockField {
return field.type === 'blocks'
}
export function fieldIsGroupType(field: Field): field is GroupField {
return field.type === 'group'
}
export function optionIsObject(option: Option): option is OptionObject {
return typeof option === 'object'
}

View File

@@ -5,10 +5,10 @@ import translations from './index'
export const defaultOptions: InitOptions = {
debug: false,
detection: {
caches: ['header', 'cookie', 'localStorage'],
caches: ['cookie', 'localStorage', 'header'],
lookupCookie: 'lng',
lookupLocalStorage: 'lng',
order: ['header', 'cookie', 'localStorage'],
order: ['cookie', 'localStorage', 'header'],
},
fallbackLng: 'en',
interpolation: {

View File

@@ -10,6 +10,9 @@ export async function getFilePrefix({
const imageSizes = (collection?.upload as IncomingUploadType)?.imageSizes || []
const files = await req.payload.find({
collection: collection.slug,
depth: 0,
limit: 1,
pagination: false,
where: {
or: [
{

View File

@@ -1,7 +1,7 @@
{
"name": "@payloadcms/plugin-form-builder",
"description": "Form builder plugin for Payload CMS",
"version": "1.1.1",
"version": "1.2.0",
"homepage:": "https://payloadcms.com",
"repository": "git@github.com:payloadcms/plugin-form-builder.git",
"main": "dist/index.js",

View File

@@ -7,6 +7,8 @@ import sendEmail from './hooks/sendEmail'
// all settings can be overridden by the config
export const generateSubmissionCollection = (formConfig: PluginConfig): CollectionConfig => {
const formSlug = formConfig?.formOverrides?.slug || 'forms'
const newConfig: CollectionConfig = {
...(formConfig?.formSubmissionOverrides || {}),
access: {
@@ -25,9 +27,28 @@ export const generateSubmissionCollection = (formConfig: PluginConfig): Collecti
admin: {
readOnly: true,
},
relationTo: formConfig?.formOverrides?.slug || 'forms',
relationTo: formSlug,
required: true,
type: 'relationship',
validate: async (value, { payload }) => {
/* Don't run in the client side */
if (!payload) return true
if (payload) {
let existingForm
try {
existingForm = await payload.findByID({
id: value,
collection: formSlug,
})
return true
} catch (error) {
return 'Cannot create this submission because this form does not exist.'
}
}
},
},
{
name: 'submissionData',

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-seo",
"version": "2.1.0",
"version": "2.2.0",
"homepage:": "https://payloadcms.com",
"repository": "git@github.com:payloadcms/plugin-seo.git",
"description": "SEO plugin for Payload",

View File

@@ -22,7 +22,7 @@ type MetaDescriptionProps = TextareaField & {
}
export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
const { name, label, path, pluginConfig } = props
const { name, label, path, pluginConfig, required } = props
const { t } = useTranslation('plugin-seo')
@@ -36,7 +36,7 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
path,
} as Options)
const { setValue, showError, value } = field
const { setValue, showError, value, errorMessage } = field
const regenerateDescription = useCallback(async () => {
const { generateDescription } = pluginConfig
@@ -67,6 +67,18 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
>
<div>
{label && typeof label === 'string' && label}
{required && (
<span
style={{
marginLeft: '5px',
color: 'var(--theme-error-500)',
}}
>
*
</span>
)}
{typeof pluginConfig.generateDescription === 'function' && (
<React.Fragment>
&nbsp; &mdash; &nbsp;
@@ -117,6 +129,8 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
style={{
marginBottom: 0,
}}
required={required}
errorMessage={errorMessage}
value={value}
/>
</div>

View File

@@ -19,7 +19,7 @@ type MetaImageProps = UploadInputProps & {
}
export const MetaImage: React.FC<MetaImageProps> = (props) => {
const { name, fieldTypes, label, pluginConfig, relationTo } = props || {}
const { name, fieldTypes, label, pluginConfig, relationTo, required } = props || {}
const field: FieldType<string> = useField(props as Options)
@@ -29,7 +29,7 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
const [fields] = useAllFormFields()
const docInfo = useDocumentInfo()
const { setValue, showError, value } = field
const { setValue, showError, value, errorMessage } = field
const regenerateImage = useCallback(async () => {
const { generateImage } = pluginConfig
@@ -68,6 +68,18 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
>
<div>
{label && typeof label === 'string' && label}
{required && (
<span
style={{
marginLeft: '5px',
color: 'var(--theme-error-500)',
}}
>
*
</span>
)}
{typeof pluginConfig.generateImage === 'function' && (
<React.Fragment>
&nbsp; &mdash; &nbsp;
@@ -110,6 +122,8 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
collection={collection}
fieldTypes={fieldTypes}
filterOptions={{}}
errorMessage={errorMessage}
required={required}
label={undefined}
name={name}
onChange={(incomingImage) => {

View File

@@ -25,7 +25,7 @@ type MetaTitleProps = TextFieldType & {
}
export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
const { name, label, path, pluginConfig } = props || {}
const { name, label, path, pluginConfig, required } = props || {}
const { t } = useTranslation('plugin-seo')
@@ -39,7 +39,7 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
const [fields] = useAllFormFields()
const docInfo = useDocumentInfo()
const { setValue, showError, value } = field
const { setValue, showError, value, errorMessage } = field
const regenerateTitle = useCallback(async () => {
const { generateTitle } = pluginConfig
@@ -70,6 +70,18 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
>
<div>
{label && typeof label === 'string' && label}
{required && (
<span
style={{
marginLeft: '5px',
color: 'var(--theme-error-500)',
}}
>
*
</span>
)}
{typeof pluginConfig.generateTitle === 'function' && (
<React.Fragment>
&nbsp; &mdash; &nbsp;
@@ -121,6 +133,8 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
style={{
marginBottom: 0,
}}
errorMessage={errorMessage}
required={required}
value={value}
/>
</div>

View File

@@ -30,6 +30,7 @@ const seo =
},
label: 'Overview',
},
// @ts-expect-error
{
name: 'title',
type: 'text',
@@ -39,6 +40,7 @@ const seo =
},
},
localized: true,
...(pluginConfig?.fieldOverrides?.title ?? {}),
},
{
name: 'description',
@@ -49,6 +51,7 @@ const seo =
},
},
localized: true,
...(pluginConfig?.fieldOverrides?.description ?? {}),
},
...(pluginConfig?.uploadsCollection
? [
@@ -66,6 +69,7 @@ const seo =
label: 'Meta Image',
localized: true,
relationTo: pluginConfig?.uploadsCollection,
...(pluginConfig?.fieldOverrides?.image ?? {}),
} as Field,
]
: []),
@@ -81,6 +85,7 @@ const seo =
label: 'Preview',
},
],
interfaceName: pluginConfig.interfaceName,
label: 'SEO',
},
]

View File

@@ -1,5 +1,5 @@
import type { ContextType } from 'payload/dist/admin/components/utilities/DocumentInfo/types'
import type { Field } from 'payload/dist/fields/config/types'
import type { Field, TextareaField, TextField, UploadField } from 'payload/dist/fields/config/types'
export type GenerateTitle = <T = any>(
args: ContextType & { doc: T; locale?: string },
@@ -29,6 +29,12 @@ export interface PluginConfig {
generateURL?: GenerateURL
globals?: string[]
tabbedUI?: boolean
fieldOverrides?: {
title?: Partial<TextField>
description?: Partial<TextareaField>
image?: Partial<UploadField>
}
interfaceName?: string
uploadsCollection?: string
}

View File

@@ -37,7 +37,7 @@ export const Preview: React.FC<PreviewProps> = (props) => {
if (typeof generateURL === 'function' && !href) {
const newHref = await generateURL({
...docInfo,
doc: { fields },
doc: { ...fields },
locale: typeof locale === 'object' ? locale?.code : locale,
})

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "0.5.1",
"version": "0.5.2",
"description": "The officially supported Lexical richtext adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

632
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,10 +4,10 @@ import { usersCollectionSlug } from '../slugs'
export const Users: CollectionConfig = {
slug: usersCollectionSlug,
auth: true,
admin: {
useAsTitle: 'email',
},
auth: true,
fields: [
{
name: 'textField',

View File

@@ -16,13 +16,21 @@ const bundlerAdapters = {
webpack: webpackBundler(),
}
const [testSuiteDir] = process.argv.slice(4)
const migrationDir = path.resolve(
(process.env.PAYLOAD_CONFIG_PATH
? path.join(process.env.PAYLOAD_CONFIG_PATH, '..')
: testSuiteDir) || __dirname,
'migrations',
)
const databaseAdapters = {
mongoose: mongooseAdapter({
migrationDir: path.resolve(__dirname, '../packages/db-mongodb/migrations'),
migrationDir,
url: 'mongodb://127.0.0.1/payloadtests',
}),
postgres: postgresAdapter({
migrationDir: path.resolve(__dirname, '../packages/db-postgres/migrations'),
migrationDir,
pool: {
connectionString: process.env.POSTGRES_URL || 'postgres://127.0.0.1:5432/payloadtests',
},
@@ -33,6 +41,7 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
const [name] = process.argv.slice(2)
const config: Config = {
db: databaseAdapters[process.env.PAYLOAD_DATABASE || 'mongoose'],
editor: slateEditor({}),
rateLimit: {
max: 9999999999,
@@ -40,7 +49,6 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
},
telemetry: false,
...testConfig,
db: databaseAdapters[process.env.PAYLOAD_DATABASE || 'mongoose'],
}
config.admin = {

View File

@@ -41,7 +41,7 @@ export default buildConfigWithDefaults({
{
name: 'blockTwoField',
type: 'text',
}
},
],
custom: { description: 'The blockOne of this page' },
},

View File

@@ -1,6 +1,7 @@
import type { BlockField } from 'payload/types'
import payload from '../../packages/payload/src'
import { initPayloadTest } from '../helpers/configHelpers'
import { BlockField } from "payload/dist/fields/config/types";
require('isomorphic-fetch')
@@ -39,7 +40,7 @@ describe('Config', () => {
it('allows a custom field in collection fields', () => {
const [collection] = payload.config.collections
const [field,] = collection.fields
const [field] = collection.fields
expect(field.custom).toEqual({ description: 'The title of this page' })
})
@@ -48,7 +49,9 @@ describe('Config', () => {
const [collection] = payload.config.collections
const [, blocksField] = collection.fields
expect((blocksField as BlockField).blocks[0].custom).toEqual({ description: 'The blockOne of this page' })
expect((blocksField as BlockField).blocks[0].custom).toEqual({
description: 'The blockOne of this page',
})
})
})

View File

@@ -8,77 +8,76 @@
export interface Config {
collections: {
pages: Page;
users: User;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
pages: Page
users: User
'payload-preferences': PayloadPreference
'payload-migrations': PayloadMigration
}
globals: {
'my-global': MyGlobal;
};
'my-global': MyGlobal
}
}
export interface Page {
id: string;
title?: string | null;
id: string
title?: string | null
myBlocks?:
| {
blockOneField?: string | null;
blockTwoField?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'blockOne';
blockOneField?: string | null
blockTwoField?: string | null
id?: string | null
blockName?: string | null
blockType: 'blockOne'
}[]
| null;
updatedAt: string;
createdAt: string;
| null
updatedAt: string
createdAt: string
}
export interface User {
id: string;
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;
id: string
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
}
export interface PayloadPreference {
id: string;
id: string
user: {
relationTo: 'users';
value: string | User;
};
key?: string | null;
relationTo: 'users'
value: string | User
}
key?: string | null
value?:
| {
[k: string]: unknown;
[k: string]: unknown
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
| null
updatedAt: string
createdAt: string
}
export interface PayloadMigration {
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
id: string
name?: string | null
batch?: number | null
updatedAt: string
createdAt: string
}
export interface MyGlobal {
id: string;
title?: string | null;
updatedAt?: string | null;
createdAt?: string | null;
id: string
title?: string | null
updatedAt?: string | null
createdAt?: string | null
}
declare module 'payload' {
export interface GeneratedTypes extends Config {}
}

1
test/database/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
migrations

View File

@@ -4,14 +4,16 @@ import { devUser } from '../credentials'
export default buildConfigWithDefaults({
collections: [
{
slug: 'posts',
fields: [
{
name: 'title',
required: true,
type: 'text',
required: true,
},
{
name: 'throwAfterChange',
type: 'checkbox',
defaultValue: false,
hooks: {
afterChange: [
@@ -22,17 +24,16 @@ export default buildConfigWithDefaults({
},
],
},
type: 'checkbox',
},
],
slug: 'posts',
},
{
slug: 'relation-a',
fields: [
{
name: 'relationship',
relationTo: 'relation-b',
type: 'relationship',
relationTo: 'relation-b',
},
{
name: 'richText',
@@ -43,14 +44,14 @@ export default buildConfigWithDefaults({
plural: 'Relation As',
singular: 'Relation A',
},
slug: 'relation-a',
},
{
slug: 'relation-b',
fields: [
{
name: 'relationship',
relationTo: 'relation-a',
type: 'relationship',
relationTo: 'relation-a',
},
{
name: 'richText',
@@ -61,7 +62,6 @@ export default buildConfigWithDefaults({
plural: 'Relation Bs',
singular: 'Relation B',
},
slug: 'relation-b',
},
],
onInit: async (payload) => {

View File

@@ -1,13 +1,19 @@
import { sql } from 'drizzle-orm'
import fs from 'fs'
import { GraphQLClient } from 'graphql-request'
import path from 'path'
import type { DrizzleDB } from '../../packages/db-postgres/src/types'
import type { TypeWithID } from '../../packages/payload/src/collections/config/types'
import type { PayloadRequest } from '../../packages/payload/src/express/types'
import payload from '../../packages/payload/src'
import { migrate } from '../../packages/payload/src/bin/migrate'
import { commitTransaction } from '../../packages/payload/src/utilities/commitTransaction'
import { initTransaction } from '../../packages/payload/src/utilities/initTransaction'
import { devUser } from '../credentials'
import { initPayloadTest } from '../helpers/configHelpers'
import removeFiles from '../helpers/removeFiles'
describe('database', () => {
let serverURL
@@ -16,16 +22,12 @@ describe('database', () => {
const collection = 'posts'
const title = 'title'
let user: TypeWithID & Record<string, unknown>
let useTransactions = true
beforeAll(async () => {
const init = await initPayloadTest({ __dirname, init: { local: false } })
serverURL = init.serverURL
const url = `${serverURL}/api/graphql`
client = new GraphQLClient(url)
if (payload.db.name === 'mongoose') {
useTransactions = false
}
const loginResult = await payload.login({
collection: 'users',
@@ -39,6 +41,101 @@ describe('database', () => {
user = loginResult.user
})
describe('migrations', () => {
beforeAll(async () => {
if (process.env.PAYLOAD_DROP_DATABASE === 'true' && 'drizzle' in payload.db) {
const drizzle = payload.db.drizzle as DrizzleDB
// @ts-expect-error drizzle raw sql typing
await drizzle.execute(sql`drop schema public cascade;
create schema public;`)
}
})
afterAll(() => {
removeFiles(path.normalize(payload.db.migrationDir))
})
it('should run migrate:create', async () => {
const args = {
_: ['migrate:create', 'test'],
forceAcceptWarning: true,
}
await migrate(args)
// read files names in migrationsDir
const migrationFile = path.normalize(fs.readdirSync(payload.db.migrationDir)[0])
expect(migrationFile).toContain('_test')
})
it('should run migrate', async () => {
const args = {
_: ['migrate'],
}
await migrate(args)
const { docs } = await payload.find({
collection: 'payload-migrations',
})
const migration = docs[0]
expect(migration.name).toContain('_test')
expect(migration.batch).toStrictEqual(1)
})
it('should run migrate:status', async () => {
let error
const args = {
_: ['migrate:status'],
}
try {
await migrate(args)
} catch (e) {
error = e
}
expect(error).toBeUndefined()
})
it('should run migrate:fresh', async () => {
const args = {
_: ['migrate:fresh'],
forceAcceptWarning: true,
}
await migrate(args)
const { docs } = await payload.find({
collection: 'payload-migrations',
})
const migration = docs[0]
expect(migration.name).toContain('_test')
expect(migration.batch).toStrictEqual(1)
})
// known issue: https://github.com/payloadcms/payload/issues/4597
it.skip('should run migrate:down', async () => {
let error
const args = {
_: ['migrate:down'],
}
try {
await migrate(args)
} catch (e) {
error = e
}
expect(error).toBeUndefined()
})
// known issue: https://github.com/payloadcms/payload/issues/4597
it.skip('should run migrate:refresh', async () => {
let error
const args = {
_: ['migrate:refresh'],
}
try {
await migrate(args)
} catch (e) {
error = e
}
expect(error).toBeUndefined()
})
})
describe('transactions', () => {
describe('local api', () => {
it('should commit multiple operations in isolation', async () => {
@@ -57,7 +154,6 @@ describe('database', () => {
req,
})
if (useTransactions) {
await expect(() =>
payload.findByID({
id: first.id,
@@ -65,7 +161,6 @@ describe('database', () => {
// omitting req for isolation
}),
).rejects.toThrow('The requested resource was not found.')
}
const second = await payload.create({
collection,
@@ -180,7 +275,6 @@ describe('database', () => {
// this should not do anything but is needed to be certain about the next assertion
await commitTransaction(req)
if (useTransactions) {
await expect(() =>
payload.findByID({
id: first.id,
@@ -188,7 +282,6 @@ describe('database', () => {
req,
}),
).rejects.toThrow('The requested resource was not found.')
}
})
})
})

View File

@@ -11,7 +11,6 @@ import type {
} from './payload-types'
import payload from '../../packages/payload/src'
import { mapAsync } from '../../packages/payload/src/utilities/mapAsync'
import wait from '../../packages/payload/src/utilities/wait'
import { initPageConsoleErrorCatch, openDocControls, saveDocAndAssert } from '../helpers'
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
@@ -525,10 +524,10 @@ async function clearAllDocs(): Promise<void> {
}
async function clearCollectionDocs(collectionSlug: string): Promise<void> {
const ids = (await payload.find({ collection: collectionSlug, limit: 100 })).docs.map(
(doc) => doc.id,
)
await mapAsync(ids, async (id) => {
await payload.delete({ id, collection: collectionSlug })
await payload.delete({
collection: collectionSlug,
where: {
id: { exists: true },
},
})
}

View File

@@ -246,6 +246,22 @@ const BlockFields: CollectionConfig = {
},
],
},
{
slug: 'block-b',
fields: [
{
name: 'items',
type: 'array',
fields: [
{
name: 'title2',
type: 'text',
required: true,
},
],
},
],
},
],
},
{

View File

@@ -58,6 +58,24 @@ const GroupFields: CollectionConfig = {
},
],
},
{
name: 'arrayOfGroups',
type: 'array',
defaultValue: [
{
groupItem: {
text: 'Hello world',
},
},
],
fields: [
{
name: 'groupItem',
type: 'group',
fields: [{ name: 'text', type: 'text' }],
},
],
},
{
name: 'potentiallyEmptyGroup',
type: 'group',

View File

@@ -6,7 +6,6 @@ import path from 'path'
import type { RelationshipField, TextField } from './payload-types'
import payload from '../../packages/payload/src'
import { mapAsync } from '../../packages/payload/src/utilities/mapAsync'
import wait from '../../packages/payload/src/utilities/wait'
import {
exactText,
@@ -900,6 +899,7 @@ describe('fields', () => {
await page.goto(url.list)
await page.locator('.row-1 .cell-title a').click()
}
describe('cell', () => {
test('ensure cells are smaller than 300px in height', async () => {
const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields')
@@ -1387,13 +1387,9 @@ describe('fields', () => {
afterEach(async () => {
// delete all existing relationship documents
const allRelationshipDocs = await payload.find({
await payload.delete({
collection: relationshipFieldsSlug,
limit: 100,
})
const relationshipIDs = allRelationshipDocs.docs.map((doc) => doc.id)
await mapAsync(relationshipIDs, async (id) => {
await payload.delete({ id, collection: relationshipFieldsSlug })
where: { id: { exists: true } },
})
})

View File

@@ -112,7 +112,7 @@ describe('Fields', () => {
})
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
expect(localizedDoc.localizedHasMany.en).toEqual(localizedHasMany)
})
})
@@ -413,7 +413,7 @@ describe('Fields', () => {
})
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// @ts-expect-error
expect(localizedDoc.localizedHasMany.en).toEqual(localizedHasMany)
})
})
@@ -643,6 +643,14 @@ describe('Fields', () => {
collection,
})
expect(result.items[0]).toMatchObject({
subArray: [
{
text: subArrayText,
},
],
text: 'test',
})
expect(result.items[0].subArray[0].text).toStrictEqual(subArrayText)
})
@@ -707,6 +715,15 @@ describe('Fields', () => {
expect(document.group.defaultParent).toStrictEqual(groupDefaultValue)
expect(document.group.defaultChild).toStrictEqual(groupDefaultChild)
})
it('should not have duplicate keys', async () => {
expect(document.arrayOfGroups[0]).toMatchObject({
id: expect.any(String),
groupItem: {
text: 'Hello world',
},
})
})
})
describe('tabs', () => {

View File

@@ -2,13 +2,14 @@ import fs from 'fs'
import path from 'path'
import { generateGraphQLSchema } from '../packages/payload/src/bin/generateGraphQLSchema'
import { setTestEnvPaths } from './helpers/setTestEnvPaths'
const [testConfigDir] = process.argv.slice(2)
let testDir
if (testConfigDir) {
testDir = path.resolve(__dirname, testConfigDir)
setPaths(testDir)
setTestEnvPaths(testDir)
generateGraphQLSchema()
} else {
// Generate graphql schema for entire directory
@@ -18,17 +19,7 @@ if (testConfigDir) {
.filter((f) => f.isDirectory())
.forEach((dir) => {
const suiteDir = path.resolve(testDir, dir.name)
const configFound = setPaths(suiteDir)
const configFound = setTestEnvPaths(suiteDir)
if (configFound) generateGraphQLSchema()
})
}
// Set config path and TS output path using test dir
function setPaths(dir) {
const configPath = path.resolve(dir, 'config.ts')
if (fs.existsSync(configPath)) {
process.env.PAYLOAD_CONFIG_PATH = configPath
return true
}
return false
}

View File

@@ -2,13 +2,14 @@ import fs from 'fs'
import path from 'path'
import { generateTypes } from '../packages/payload/src/bin/generateTypes'
import { setTestEnvPaths } from './helpers/setTestEnvPaths'
const [testConfigDir] = process.argv.slice(2)
let testDir
if (testConfigDir) {
testDir = path.resolve(__dirname, testConfigDir)
setPaths(testDir)
setTestEnvPaths(testDir)
generateTypes()
} else {
// Generate types for entire directory
@@ -18,19 +19,7 @@ if (testConfigDir) {
.filter((f) => f.isDirectory())
.forEach((dir) => {
const suiteDir = path.resolve(testDir, dir.name)
const configFound = setPaths(suiteDir)
const configFound = setTestEnvPaths(suiteDir)
if (configFound) generateTypes()
})
}
// Set config path and TS output path using test dir
function setPaths(dir) {
const configPath = path.resolve(dir, 'config.ts')
const outputPath = path.resolve(dir, 'payload-types.ts')
if (fs.existsSync(configPath)) {
process.env.PAYLOAD_CONFIG_PATH = configPath
process.env.PAYLOAD_TS_OUTPUT_PATH = outputPath
return true
}
return false
}

View File

@@ -0,0 +1,14 @@
// Set config path and TS output path using test dir
import fs from 'fs'
import path from 'path'
export function setTestEnvPaths(dir) {
const configPath = path.resolve(dir, 'config.ts')
const outputPath = path.resolve(dir, 'payload-types.ts')
if (fs.existsSync(configPath)) {
process.env.PAYLOAD_CONFIG_PATH = configPath
process.env.PAYLOAD_TS_OUTPUT_PATH = outputPath
return true
}
return false
}

View File

@@ -0,0 +1,12 @@
import type { AfterLoginHook } from '../../../../packages/payload/src/collections/config/types'
export const afterLoginHook: AfterLoginHook = async ({ req, user }) => {
return req.payload.update({
id: user.id,
collection: 'hooks-users',
data: {
afterLoginHook: true,
},
req,
})
}

View File

@@ -6,8 +6,9 @@ import type { Payload } from '../../../../packages/payload/src/payload'
import { AuthenticationError } from '../../../../packages/payload/src/errors'
import { devUser, regularUser } from '../../../credentials'
import { afterLoginHook } from './afterLoginHook'
const beforeLoginHook: BeforeLoginHook = ({ user, req }) => {
const beforeLoginHook: BeforeLoginHook = ({ req, user }) => {
const isAdmin = user.roles.includes('admin') ? user : undefined
if (!isAdmin) {
throw new AuthenticationError(req.t)
@@ -33,16 +34,21 @@ const Users: CollectionConfig = {
fields: [
{
name: 'roles',
label: 'Role',
type: 'select',
options: ['admin', 'user'],
defaultValue: 'user',
hasMany: true,
label: 'Role',
options: ['admin', 'user'],
required: true,
saveToJWT: true,
hasMany: true,
},
{
name: 'afterLoginHook',
type: 'checkbox',
},
],
hooks: {
afterLogin: [afterLoginHook],
beforeLogin: [beforeLoginHook],
},
}

View File

@@ -27,7 +27,7 @@ describe('Hooks', () => {
beforeAll(async () => {
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } })
const config = await configPromise
client = new RESTClient(config, { serverURL, defaultSlug: transformSlug })
client = new RESTClient(config, { defaultSlug: transformSlug, serverURL })
apiUrl = `${serverURL}/api`
})
@@ -43,8 +43,8 @@ describe('Hooks', () => {
const doc = await payload.create({
collection: transformSlug,
data: {
transform: [2, 8],
localizedTransform: [2, 8],
transform: [2, 8],
},
})
@@ -59,15 +59,15 @@ describe('Hooks', () => {
doc = await payload.create({
collection: hooksSlug,
data: {
fieldBeforeValidate: false,
collectionBeforeValidate: false,
fieldBeforeChange: false,
collectionBeforeChange: false,
fieldAfterChange: false,
collectionAfterChange: false,
collectionBeforeRead: false,
fieldAfterRead: false,
collectionAfterRead: false,
collectionBeforeChange: false,
collectionBeforeRead: false,
collectionBeforeValidate: false,
fieldAfterChange: false,
fieldAfterRead: false,
fieldBeforeChange: false,
fieldBeforeValidate: false,
},
})
@@ -84,10 +84,10 @@ describe('Hooks', () => {
const document: NestedAfterReadHook = await payload.create({
collection: nestedAfterReadHooksSlug,
data: {
text: 'ok',
group: {
array: [{ input: 'input' }],
},
text: 'ok',
},
})
@@ -106,7 +106,6 @@ describe('Hooks', () => {
const document = await payload.create({
collection: nestedAfterReadHooksSlug,
data: {
text: 'ok',
group: {
array: [
{
@@ -117,12 +116,13 @@ describe('Hooks', () => {
shouldPopulate: relation.id,
},
},
text: 'ok',
},
})
const retrievedDoc = await payload.findByID({
collection: nestedAfterReadHooksSlug,
id: document.id,
collection: nestedAfterReadHooksSlug,
})
expect(retrievedDoc.group.array[0].shouldPopulate.title).toEqual(relation.title)
@@ -138,8 +138,8 @@ describe('Hooks', () => {
})
const retrievedDoc = await payload.findByID({
collection: chainingHooksSlug,
id: document.id,
collection: chainingHooksSlug,
})
expect(retrievedDoc.text).toEqual('ok!!')
@@ -189,15 +189,15 @@ describe('Hooks', () => {
const [updatedDoc1, updatedDoc2] = await Promise.all([
await payload.update({
collection: afterOperationSlug,
id: doc1.id,
collection: afterOperationSlug,
data: {
title: 'Title',
},
}),
await payload.update({
collection: afterOperationSlug,
id: doc2.id,
collection: afterOperationSlug,
data: {
title: 'Title',
},
@@ -225,8 +225,8 @@ describe('Hooks', () => {
})
const retrievedDoc = await payload.findByID({
collection: contextHooksSlug,
id: document.id,
collection: contextHooksSlug,
})
expect(retrievedDoc.value).toEqual('secret')
@@ -235,17 +235,17 @@ describe('Hooks', () => {
it('should pass context from local API to hooks', async () => {
const document = await payload.create({
collection: contextHooksSlug,
data: {
value: 'wrongvalue',
},
context: {
secretValue: 'data from local API',
},
data: {
value: 'wrongvalue',
},
})
const retrievedDoc = await payload.findByID({
collection: contextHooksSlug,
id: document.id,
collection: contextHooksSlug,
})
expect(retrievedDoc.value).toEqual('data from local API')
@@ -282,8 +282,8 @@ describe('Hooks', () => {
const document = (await response.json()).doc
const retrievedDoc = await payload.findByID({
collection: contextHooksSlug,
id: document.id,
collection: contextHooksSlug,
})
expect(retrievedDoc.value).toEqual('data from rest API')
@@ -291,7 +291,7 @@ describe('Hooks', () => {
})
describe('auth collection hooks', () => {
it('allow admin login', async () => {
it('should call afterLogin hook', async () => {
const { user } = await payload.login({
collection: hooksUsersSlug,
data: {
@@ -299,7 +299,15 @@ describe('Hooks', () => {
password: devUser.password,
},
})
const result = await payload.findByID({
id: user.id,
collection: hooksUsersSlug,
})
expect(user).toBeDefined()
expect(user.afterLoginHook).toStrictEqual(true)
expect(result.afterLoginHook).toStrictEqual(true)
})
it('deny user login', async () => {
@@ -342,8 +350,8 @@ describe('Hooks', () => {
// BeforeRead is only run for find operations
const foundDoc = await payload.findByID({
collection: dataHooksSlug,
id: doc.id,
collection: dataHooksSlug,
})
expect(JSON.parse(foundDoc.collection_beforeRead_collection)).toStrictEqual(

View File

@@ -216,7 +216,8 @@ describe('Localization', () => {
await page.fill('#field-layout__0__text', 'test')
await saveDocAndAssert(page)
const originalDocURL = page.url()
const originalID = await page.locator('.id-label').innerText()
// duplicate
await openDocControls(page)
await page.locator('#action-duplicate').click()
@@ -229,7 +230,7 @@ describe('Localization', () => {
await expect(page.locator('.Toastify')).toContainText('successfully duplicated')
// expect that the document has a new id
expect(page.url()).not.toStrictEqual(originalDocURL)
await expect(page.locator('.id-label')).not.toContainText(originalID)
})
})
})

View File

@@ -5,6 +5,7 @@ import { serializeLexical } from '../../packages/plugin-form-builder/src/utiliti
import { serializeSlate } from '../../packages/plugin-form-builder/src/utilities/slate/serializeSlate'
import { initPayloadTest } from '../helpers/configHelpers'
import { formSubmissionsSlug, formsSlug } from './shared'
import { ValidationError } from '../../packages/payload/src/errors'
describe('Form Builder Plugin', () => {
let form: Form
@@ -42,7 +43,7 @@ describe('Form Builder Plugin', () => {
it('adds form submissions collection', async () => {
const { docs: formSubmissions } = await payload.find({ collection: formSubmissionsSlug })
expect(formSubmissions).toHaveLength(0)
expect(formSubmissions).toHaveLength(1)
})
})
@@ -118,6 +119,25 @@ describe('Form Builder Plugin', () => {
expect(formSubmission.submissionData[0]).toHaveProperty('value', 'Test Submission')
})
it('does not create a form submission for a non-existing form', async () => {
const req = async () =>
payload.create({
collection: formSubmissionsSlug,
data: {
form: '659c7c2f98ffb5d83df9dadb',
submissionData: [
{
field: 'name',
value: 'Test Submission',
},
],
},
depth: 0,
})
await expect(req).rejects.toThrow(ValidationError)
})
it('replaces curly braces with data when using slate serializer', async () => {
const mockName = 'Test Submission'
const mockEmail = 'dev@payloadcms.com'

View File

@@ -1,7 +1,7 @@
import type { Payload } from '../../../packages/payload/src'
import type { PayloadRequest } from '../../../packages/payload/src/express/types'
import { formsSlug, pagesSlug } from '../shared'
import { formSubmissionsSlug, formsSlug, pagesSlug } from '../shared'
export const seed = async (payload: Payload): Promise<boolean> => {
payload.logger.info('Seeding data...')
@@ -53,6 +53,23 @@ export const seed = async (payload: Payload): Promise<boolean> => {
},
})
await payload.create({
collection: formSubmissionsSlug,
data: {
form: formID,
submissionData: [
{
field: 'name',
value: 'Test Submission',
},
{
field: 'email',
value: 'tester@example.com',
},
],
},
})
await payload.create({
collection: pagesSlug,
data: {

View File

@@ -51,6 +51,11 @@ export default buildConfigWithDefaults({
label: 'og:title',
},
],
fieldOverrides: {
title: {
required: true,
},
},
generateTitle: (data: any) => `Website.com — ${data?.doc?.title?.value}`,
generateDescription: ({ doc }: any) => doc?.excerpt?.value,
generateURL: ({ doc, locale }: any) =>

View File

@@ -78,6 +78,15 @@ describe('SEO Plugin', () => {
await expect(metaTitle).toHaveValue('Website.com — Test Page')
})
// todo: Re-enable this test once required attributes are fixed
/* test('Title should be required as per custom override', async () => {
const metaTitleClass = '#field-title'
const metaTitle = page.locator(metaTitleClass).nth(0)
await expect(metaTitle).toHaveAttribute('required', '')
}) */
test('Indicator should be orangered and characters counted', async () => {
const indicatorClass =
'#field-meta > div > div.render-fields.render-fields--margins-small > div:nth-child(2) > div:nth-child(3) > div > div:nth-child(3) > div'

View File

@@ -31,6 +31,9 @@ describe('SEO Plugin', () => {
data: {
title: 'Test page',
slug: 'test-page',
meta: {
title: 'Test page',
},
},
depth: 0,
})

View File

@@ -43,6 +43,7 @@ export const chainedRelSlug = 'chained'
export const customIdSlug = 'custom-id'
export const customIdNumberSlug = 'custom-id-number'
export const polymorphicRelationshipsSlug = 'polymorphic-relationships'
export const treeSlug = 'tree'
export default buildConfigWithDefaults({
collections: [
@@ -244,6 +245,20 @@ export default buildConfigWithDefaults({
},
],
},
{
slug: treeSlug,
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'parent',
type: 'relationship',
relationTo: 'tree',
},
],
},
],
onInit: async (payload) => {
await payload.create({
@@ -337,5 +352,20 @@ export default buildConfigWithDefaults({
filteredRelation: filteredRelation.id,
},
})
const root = await payload.create({
collection: 'tree',
data: {
text: 'root',
},
})
await payload.create({
collection: 'tree',
data: {
text: 'sub',
parent: root.id,
},
})
},
})

View File

@@ -11,7 +11,6 @@ import type {
} from './payload-types'
import payload from '../../packages/payload/src'
import { mapAsync } from '../../packages/payload/src/utilities/mapAsync'
import { devUser } from '../credentials'
import { initPayloadTest } from '../helpers/configHelpers'
import { RESTClient } from '../helpers/rest'
@@ -22,6 +21,7 @@ import config, {
defaultAccessRelSlug,
relationSlug,
slug,
treeSlug,
} from './config'
let apiUrl
@@ -39,7 +39,7 @@ describe('Relationships', () => {
beforeAll(async () => {
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } })
apiUrl = `${serverURL}/api`
client = new RESTClient(config, { serverURL, defaultSlug: slug })
client = new RESTClient(config, { defaultSlug: slug, serverURL })
await client.login()
const response = await fetch(`${apiUrl}/users/login`, {
@@ -127,8 +127,8 @@ describe('Relationships', () => {
})
chained3 = await payload.update<ChainedRelation>({
collection: chainedRelSlug,
id: chained3.id,
collection: chainedRelSlug,
data: {
name: 'chain3',
relation: chained.id,
@@ -154,13 +154,13 @@ describe('Relationships', () => {
})
post = await createPost({
relationField: relation.id,
defaultAccessRelation: defaultAccessRelation.id,
chainedRelation: chained.id,
maxDepthRelation: relation.id,
customIdRelation: customIdRelation.id,
customIdNumberRelation: customIdNumberRelation.id,
customIdRelation: customIdRelation.id,
defaultAccessRelation: defaultAccessRelation.id,
filteredRelation: filteredRelation.id,
maxDepthRelation: relation.id,
relationField: relation.id,
})
await createPost() // Extra post to allow asserting totalDoc count
@@ -193,15 +193,15 @@ describe('Relationships', () => {
expect(docAfterUpdatingRel.filteredRelation).toMatchObject({ id: filteredRelation.id })
// Attempt to update post with a now filtered relation
const { status, errors } = await client.update<Post>({
const { errors, status } = await client.update<Post>({
id: post.id,
data: { filteredRelation: filteredRelation.id },
})
expect(errors?.[0]).toMatchObject({
name: 'ValidationError',
message: expect.any(String),
data: expect.anything(),
message: expect.any(String),
})
expect(status).toEqual(400)
})
@@ -306,8 +306,8 @@ describe('Relationships', () => {
it('should allow update removing a relationship', async () => {
const result = await client.update<Post>({
slug,
id: post.id,
slug,
data: {
relationField: null,
},
@@ -445,8 +445,8 @@ describe('Relationships', () => {
await payload.create({
collection: 'screenings',
data: {
movie: movie.id,
name: 'Pulp Fiction Screening',
movie: movie.id,
},
})
})
@@ -485,8 +485,8 @@ describe('Relationships', () => {
beforeAll(async () => {
await Promise.all(
movieList.map((movie) => {
return payload.create({
movieList.map(async (movie) => {
return await payload.create({
collection: 'movies',
data: {
name: movie,
@@ -527,8 +527,8 @@ describe('Relationships', () => {
it('should allow clearing hasMany relationships', async () => {
const fiveMovies = await payload.find({
collection: 'movies',
limit: 5,
depth: 0,
limit: 5,
})
const movieIDs = fiveMovies.docs.map((doc) => doc.id)
@@ -544,8 +544,8 @@ describe('Relationships', () => {
expect(stanley.movies).toHaveLength(5)
const stanleyNeverMadeMovies = await payload.update({
collection: 'directors',
id: stanley.id,
collection: 'directors',
data: {
movies: null,
},
@@ -554,6 +554,64 @@ describe('Relationships', () => {
expect(stanleyNeverMadeMovies.movies).toHaveLength(0)
})
})
describe('Hierarchy', () => {
it('finds 1 root item with equals', async () => {
const {
docs: [item],
totalDocs: count,
} = await payload.find({
collection: treeSlug,
where: {
parent: { equals: null },
},
})
expect(count).toBe(1)
expect(item.text).toBe('root')
})
it('finds 1 root item with exists', async () => {
const {
docs: [item],
totalDocs: count,
} = await payload.find({
collection: treeSlug,
where: {
parent: { exists: false },
},
})
expect(count).toBe(1)
expect(item.text).toBe('root')
})
it('finds 1 sub item with equals', async () => {
const {
docs: [item],
totalDocs: count,
} = await payload.find({
collection: treeSlug,
where: {
parent: { not_equals: null },
},
})
expect(count).toBe(1)
expect(item.text).toBe('sub')
})
it('finds 1 sub item with exists', async () => {
const {
docs: [item],
totalDocs: count,
} = await payload.find({
collection: treeSlug,
where: {
parent: { exists: true },
},
})
expect(count).toBe(1)
expect(item.text).toBe('sub')
})
})
})
describe('Creating', () => {
@@ -562,18 +620,18 @@ describe('Relationships', () => {
const req = {} as PayloadRequest
req.transactionID = await payload.db.beginTransaction?.()
const related = await payload.create({
req,
collection: relationSlug,
data: {
name: 'parent',
},
req,
})
const withRelation = await payload.create({
req,
collection: slug,
data: {
filteredRelation: related.id,
},
req,
})
if (req.transactionID) {
@@ -597,8 +655,8 @@ describe('Relationships', () => {
collection: 'polymorphic-relationships',
data: {
polymorphic: {
value: movie.id,
relationTo: 'movies',
value: movie.id,
},
},
})
@@ -650,9 +708,8 @@ async function createPost(overrides?: Partial<Post>) {
}
async function clearDocs(): Promise<void> {
const allDocs = await payload.find({ collection: slug, limit: 100 })
const ids = allDocs.docs.map((doc) => doc.id)
await mapAsync(ids, async (id) => {
await payload.delete({ collection: slug, id })
await payload.delete({
collection: slug,
where: { id: { exists: true } },
})
}

View File

@@ -1,3 +1,4 @@
media
uploads
versions
/media-gif

View File

@@ -7,12 +7,11 @@ import removeFiles from '../helpers/removeFiles'
import { Uploads1 } from './collections/Upload1'
import Uploads2 from './collections/Upload2'
import AdminThumbnailCol from './collections/admin-thumbnail'
import { audioSlug, enlargeSlug, mediaSlug, reduceSlug, relationSlug } from './shared'
import { audioSlug, enlargeSlug, mediaSlug, reduceSlug, relationSlug, versionSlug } from './shared'
const mockModulePath = path.resolve(__dirname, './mocks/mockFSModule.js')
export default buildConfigWithDefaults({
serverURL: undefined,
admin: {
webpack: (config) => ({
...config,
@@ -34,6 +33,11 @@ export default buildConfigWithDefaults({
type: 'upload',
relationTo: 'media',
},
{
name: 'versionedImage',
type: 'upload',
relationTo: versionSlug,
},
],
},
{
@@ -42,164 +46,157 @@ export default buildConfigWithDefaults({
{
name: 'audio',
type: 'upload',
relationTo: 'media',
filterOptions: {
mimeType: {
in: ['audio/mpeg'],
},
},
relationTo: 'media',
},
],
},
{
slug: 'gif-resize',
fields: [],
upload: {
staticURL: '/media-gif',
staticDir: './media-gif',
mimeTypes: ['image/gif'],
resizeOptions: {
position: 'center',
width: 200,
height: 200,
},
formatOptions: {
format: 'gif',
},
imageSizes: [
{
name: 'small',
width: 100,
height: 100,
formatOptions: { format: 'gif', options: { quality: 90 } },
height: 100,
width: 100,
},
{
name: 'large',
width: 1000,
height: 1000,
formatOptions: { format: 'gif', options: { quality: 90 } },
height: 1000,
width: 1000,
},
],
mimeTypes: ['image/gif'],
resizeOptions: {
height: 200,
position: 'center',
width: 200,
},
staticDir: './media-gif',
staticURL: '/media-gif',
},
fields: [],
},
{
slug: 'no-image-sizes',
fields: [],
upload: {
staticURL: '/no-image-sizes',
staticDir: './no-image-sizes',
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
resizeOptions: {
height: 200,
position: 'center',
width: 200,
height: 200,
},
staticDir: './no-image-sizes',
staticURL: '/no-image-sizes',
},
fields: [],
},
{
slug: 'object-fit',
fields: [],
upload: {
staticURL: '/object-fit',
staticDir: './object-fit',
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
imageSizes: [
{
name: 'fitContain',
width: 400,
height: 300,
fit: 'contain',
height: 300,
width: 400,
},
{
name: 'fitInside',
width: 300,
height: 400,
fit: 'inside',
height: 400,
width: 300,
},
{
name: 'fitCover',
width: 900,
height: 300,
fit: 'cover',
height: 300,
width: 900,
},
{
name: 'fitOutside',
width: 900,
height: 200,
fit: 'outside',
height: 200,
width: 900,
},
],
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
staticDir: './object-fit',
staticURL: '/object-fit',
},
fields: [],
},
{
slug: 'crop-only',
fields: [],
upload: {
focalPoint: false,
staticURL: '/crop-only',
staticDir: './crop-only',
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
imageSizes: [
{
name: 'focalTest',
width: 400,
height: 300,
width: 400,
},
{
name: 'focalTest2',
width: 600,
height: 300,
width: 600,
},
{
name: 'focalTest3',
width: 900,
height: 300,
width: 900,
},
],
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
staticDir: './crop-only',
staticURL: '/crop-only',
},
fields: [],
},
{
slug: 'focal-only',
fields: [],
upload: {
crop: false,
staticURL: '/focal-only',
staticDir: './focal-only',
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
imageSizes: [
{
name: 'focalTest',
width: 400,
height: 300,
width: 400,
},
{
name: 'focalTest2',
width: 600,
height: 300,
width: 600,
},
{
name: 'focalTest3',
width: 900,
height: 300,
width: 900,
},
],
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
staticDir: './focal-only',
staticURL: '/focal-only',
},
fields: [],
},
{
slug: mediaSlug,
fields: [],
upload: {
staticURL: '/media',
staticDir: './media',
staticURL: '/media',
// crop: false,
// focalPoint: false,
mimeTypes: [
'image/png',
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'audio/mpeg',
],
formatOptions: {
format: 'png',
options: { quality: 90 },
@@ -207,143 +204,133 @@ export default buildConfigWithDefaults({
imageSizes: [
{
name: 'maintainedAspectRatio',
width: 1024,
height: undefined,
crop: 'center',
position: 'center',
formatOptions: { format: 'png', options: { quality: 90 } },
height: undefined,
position: 'center',
width: 1024,
},
{
name: 'differentFormatFromMainImage',
width: 200,
height: undefined,
formatOptions: { format: 'jpg', options: { quality: 90 } },
height: undefined,
width: 200,
},
{
name: 'maintainedImageSize',
width: undefined,
height: undefined,
width: undefined,
},
{
name: 'maintainedImageSizeWithNewFormat',
width: undefined,
height: undefined,
formatOptions: { format: 'jpg', options: { quality: 90 } },
height: undefined,
width: undefined,
},
{
name: 'accidentalSameSize',
width: 320,
height: 80,
position: 'top',
width: 320,
},
{
name: 'tablet',
width: 640,
height: 480,
width: 640,
},
{
name: 'mobile',
width: 320,
height: 240,
crop: 'left top',
height: 240,
width: 320,
},
{
name: 'icon',
width: 16,
height: 16,
width: 16,
},
{
name: 'focalTest',
width: 400,
height: 300,
width: 400,
},
{
name: 'focalTest2',
width: 600,
height: 300,
width: 600,
},
{
name: 'focalTest3',
width: 900,
height: 300,
width: 900,
},
{
name: 'focalTest4',
width: 300,
height: 400,
width: 300,
},
{
name: 'focalTest5',
width: 300,
height: 600,
width: 300,
},
{
name: 'focalTest6',
width: 300,
height: 800,
width: 300,
},
{
name: 'focalTest7',
width: 300,
height: 300,
width: 300,
},
],
mimeTypes: [
'image/png',
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'audio/mpeg',
],
},
fields: [],
},
{
slug: enlargeSlug,
fields: [],
upload: {
staticURL: '/enlarge',
staticDir: './media/enlarge',
mimeTypes: [
'image/png',
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'audio/mpeg',
],
imageSizes: [
{
name: 'accidentalSameSize',
width: 320,
height: 80,
width: 320,
withoutEnlargement: false,
},
{
name: 'sameSizeWithNewFormat',
width: 320,
height: 80,
formatOptions: { format: 'jpg', options: { quality: 90 } },
height: 80,
width: 320,
withoutEnlargement: false,
},
{
name: 'resizedLarger',
width: 640,
height: 480,
width: 640,
withoutEnlargement: false,
},
{
name: 'resizedSmaller',
width: 180,
height: 50,
width: 180,
},
{
name: 'widthLowerHeightLarger',
width: 300,
height: 300,
fit: 'contain',
height: 300,
width: 300,
},
],
},
fields: [],
},
{
slug: reduceSlug,
upload: {
staticURL: '/reduce',
staticDir: './media/reduce',
mimeTypes: [
'image/png',
'image/jpg',
@@ -352,105 +339,135 @@ export default buildConfigWithDefaults({
'image/svg+xml',
'audio/mpeg',
],
staticDir: './media/enlarge',
staticURL: '/enlarge',
},
},
{
slug: reduceSlug,
fields: [],
upload: {
imageSizes: [
{
name: 'accidentalSameSize',
width: 320,
height: 80,
width: 320,
withoutEnlargement: false,
},
{
name: 'sameSizeWithNewFormat',
width: 320,
height: 80,
formatOptions: { format: 'jpg', options: { quality: 90 } },
height: 80,
width: 320,
withoutReduction: true,
},
{
name: 'resizedLarger',
width: 640,
height: 480,
width: 640,
},
{
name: 'resizedSmaller',
width: 180,
height: 50,
width: 180,
withoutReduction: true,
},
],
mimeTypes: [
'image/png',
'image/jpg',
'image/jpeg',
'image/gif',
'image/svg+xml',
'audio/mpeg',
],
staticDir: './media/reduce',
staticURL: '/reduce',
},
fields: [],
},
{
slug: 'media-trim',
fields: [],
upload: {
staticURL: '/media-trim',
staticDir: './media-trim',
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
trimOptions: 0,
imageSizes: [
{
name: 'trimNumber',
width: 1024,
height: undefined,
trimOptions: 0,
width: 1024,
},
{
name: 'trimString',
width: 1024,
height: undefined,
trimOptions: 0,
width: 1024,
},
{
name: 'trimOptions',
width: 1024,
height: undefined,
trimOptions: {
background: '#000000',
threshold: 50,
},
width: 1024,
},
],
mimeTypes: ['image/png', 'image/jpg', 'image/jpeg'],
staticDir: './media-trim',
staticURL: '/media-trim',
trimOptions: 0,
},
fields: [],
},
{
slug: 'unstored-media',
upload: {
staticURL: '/media',
disableLocalStorage: true,
},
fields: [],
upload: {
disableLocalStorage: true,
staticURL: '/media',
},
},
{
slug: 'externally-served-media',
fields: [],
upload: {
// Either use another web server like `npx serve -l 4000` (http://localhost:4000) or use the static server from the previous collection to serve the media folder (http://localhost:3000/media)
staticURL: 'http://localhost:3000/media',
staticDir: './media',
staticURL: 'http://localhost:3000/media',
},
fields: [],
},
Uploads1,
Uploads2,
AdminThumbnailCol,
{
slug: 'optional-file',
upload: {
staticURL: '/optional',
staticDir: './optional',
filesRequiredOnCreate: false,
},
fields: [],
upload: {
filesRequiredOnCreate: false,
staticDir: './optional',
staticURL: '/optional',
},
},
{
slug: 'required-file',
upload: {
staticURL: '/required',
staticDir: './required',
filesRequiredOnCreate: true,
},
fields: [],
upload: {
filesRequiredOnCreate: true,
staticDir: './required',
staticURL: '/required',
},
},
{
slug: versionSlug,
fields: [
{
name: 'title',
type: 'text',
},
],
upload: true,
versions: {
drafts: true,
},
},
],
onInit: async (payload) => {
@@ -475,10 +492,20 @@ export default buildConfigWithDefaults({
file: imageFile,
})
const { id: versionedImage } = await payload.create({
collection: versionSlug,
data: {
_status: 'published',
title: 'upload',
},
file: imageFile,
})
await payload.create({
collection: relationSlug,
data: {
image: uploadedImage,
versionedImage,
},
})
@@ -518,4 +545,5 @@ export default buildConfigWithDefaults({
},
})
},
serverURL: undefined,
})

View File

@@ -29,7 +29,7 @@ describe('uploads', () => {
beforeAll(async ({ browser }) => {
const { serverURL } = await initPayloadE2E(__dirname)
client = new RESTClient(null, { serverURL, defaultSlug: 'users' })
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
await client.login()
mediaURL = new AdminUrlUtil(serverURL, mediaSlug)
@@ -149,6 +149,33 @@ describe('uploads', () => {
await expect(iconMeta).toContainText('16x16')
})
test('should show draft uploads in the relation list', async () => {
await page.goto(relationURL.list)
// from the list edit the first document
await page.locator('.row-1 a').click()
// edit the versioned image
await page.locator('.field-versionedImage .icon--edit').click()
// fill the title with 'draft'
await page.locator('#field-title').fill('draft')
// save draft
await page.locator('#action-save-draft').click()
// close the drawer
await page.locator('.doc-drawer__header-close').click()
// remove the selected versioned image
await page.locator('.field-versionedImage .icon--x').click()
// choose from existing
await page.locator('.list-drawer__toggler').click()
await expect(page.locator('.cell-title')).toContainText('draft')
})
test('should restrict mimetype based on filterOptions', async () => {
await page.goto(audioURL.edit(audioDoc.id))
await wait(200)
@@ -214,21 +241,21 @@ describe('uploads', () => {
describe('image manipulation', () => {
test('should crop image correctly', async () => {
const positions = {
'top-left': {
focalX: 25,
focalY: 25,
dragX: 0,
dragY: 0,
},
'bottom-right': {
focalX: 75,
focalY: 75,
dragX: 800,
dragY: 800,
focalX: 75,
focalY: 75,
},
'top-left': {
dragX: 0,
dragY: 0,
focalX: 25,
focalY: 25,
},
}
const createFocalCrop = async (page: Page, position: 'bottom-right' | 'top-left') => {
const { focalX, focalY, dragX, dragY } = positions[position]
const { dragX, dragY, focalX, focalY } = positions[position]
await page.goto(mediaURL.create)
// select and upload file

View File

@@ -9,3 +9,5 @@ export const enlargeSlug = 'enlarge'
export const reduceSlug = 'reduce'
export const adminThumbnailSlug = 'admin-thumbnail'
export const versionSlug = 'versions'