Compare commits
23 Commits
payload/2.
...
plugin-clo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
003ad065c3 | ||
|
|
70715926a8 | ||
|
|
b3a6bfacf2 | ||
|
|
e1d9accb27 | ||
|
|
f2f55a84cc | ||
|
|
eba53ba60a | ||
|
|
f73d503fec | ||
|
|
6930c4e9f2 | ||
|
|
3eb681e847 | ||
|
|
cb4638cfa1 | ||
|
|
b40e9f85a2 | ||
|
|
e5a7907a72 | ||
|
|
3f25d1ca84 | ||
|
|
d5720bea7b | ||
|
|
8ce15c8b07 | ||
|
|
9f5efef78f | ||
|
|
dfba5222f3 | ||
|
|
b99d24fcfa | ||
|
|
836ed77568 | ||
|
|
1c5d5b07c8 | ||
|
|
da5f1f2240 | ||
|
|
c84c58c7b4 | ||
|
|
1c1b8f3cec |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,11 +1,28 @@
|
||||
## [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
|
||||
* **plugin-seo:** allow field and interface overrides
|
||||
* **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
|
||||
|
||||
@@ -14,6 +31,7 @@
|
||||
* **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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -11,25 +11,30 @@ 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
|
||||
|
||||
const { confirm: acceptWarning } = await prompts(
|
||||
{
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
initial: false,
|
||||
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
if (!forceAcceptWarning) {
|
||||
const { confirm: acceptWarning } = await prompts(
|
||||
{
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
initial: false,
|
||||
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
|
||||
},
|
||||
},
|
||||
)
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!acceptWarning) {
|
||||
process.exit(0)
|
||||
if (!acceptWarning) {
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
payload.logger.info({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "0.3.1",
|
||||
"version": "0.5.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -53,7 +53,7 @@ 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)) {
|
||||
@@ -95,7 +95,7 @@ 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',
|
||||
|
||||
@@ -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}` })
|
||||
|
||||
@@ -14,25 +14,30 @@ 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
|
||||
|
||||
const { confirm: acceptWarning } = await prompts(
|
||||
{
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
initial: false,
|
||||
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
if (forceAcceptWarning === false) {
|
||||
const { confirm: acceptWarning } = await prompts(
|
||||
{
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
initial: false,
|
||||
message: `WARNING: This will drop your database and run all migrations. Are you sure you want to proceed?`,
|
||||
},
|
||||
},
|
||||
)
|
||||
{
|
||||
onCancel: () => {
|
||||
process.exit(0)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!acceptWarning) {
|
||||
process.exit(0)
|
||||
if (!acceptWarning) {
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
payload.logger.info({
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -10,14 +10,15 @@ 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
|
||||
logger?: DrizzleConfig['logger']
|
||||
push?: boolean
|
||||
}
|
||||
|
||||
@@ -49,8 +50,13 @@ export type DrizzleTransaction = PgTransaction<
|
||||
|
||||
export type PostgresAdapter = BaseDatabaseAdapter & {
|
||||
drizzle: DrizzleDB
|
||||
logger: DrizzleConfig['logger']
|
||||
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
|
||||
@@ -64,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
|
||||
@@ -82,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>
|
||||
@@ -94,6 +96,5 @@ declare module 'payload' {
|
||||
}
|
||||
}
|
||||
tables: Record<string, GenericTable>
|
||||
fieldConstraints: Record<string, Record<string, string>>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.8.2",
|
||||
"version": "2.9.0",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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)
|
||||
}, [
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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={[]}
|
||||
|
||||
@@ -137,6 +137,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
fieldBaseClass,
|
||||
baseClass,
|
||||
className,
|
||||
`field-${path.replace(/\./g, '__')}`,
|
||||
showError && 'error',
|
||||
readOnly && 'read-only',
|
||||
]
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,5 +52,6 @@ export const incrementLoginAttempts = async ({
|
||||
id: doc.id,
|
||||
collection: collection.slug,
|
||||
data,
|
||||
req,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function createDatabaseAdapter<T extends BaseDatabaseAdapter>(
|
||||
createMigration,
|
||||
migrate,
|
||||
migrateDown,
|
||||
migrateFresh: async () => null,
|
||||
migrateFresh: async ({ forceAcceptWarning = null }) => null,
|
||||
migrateRefresh,
|
||||
migrateReset,
|
||||
migrateStatus,
|
||||
|
||||
@@ -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.`)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -109,6 +109,7 @@ export {
|
||||
fieldHasSubFields,
|
||||
fieldIsArrayType,
|
||||
fieldIsBlockType,
|
||||
fieldIsGroupType,
|
||||
fieldIsLocalized,
|
||||
fieldIsPresentationalOnly,
|
||||
fieldSupportsMany,
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"version": "1.1.2",
|
||||
"version": "1.2.0",
|
||||
"homepage:": "https://payloadcms.com",
|
||||
"repository": "git@github.com:payloadcms/plugin-form-builder.git",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-slate",
|
||||
"version": "1.3.1",
|
||||
"version": "1.4.0",
|
||||
"description": "The officially supported Slate richtext adapter for Payload",
|
||||
"repository": "https://github.com/payloadcms/payload",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -4,10 +4,10 @@ import { usersCollectionSlug } from '../slugs'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: usersCollectionSlug,
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
auth: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'textField',
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
1
test/database/.gitignore
vendored
Normal file
1
test/database/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
migrations
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,15 +154,13 @@ describe('database', () => {
|
||||
req,
|
||||
})
|
||||
|
||||
if (useTransactions) {
|
||||
await expect(() =>
|
||||
payload.findByID({
|
||||
id: first.id,
|
||||
collection,
|
||||
// omitting req for isolation
|
||||
}),
|
||||
).rejects.toThrow('The requested resource was not found.')
|
||||
}
|
||||
await expect(() =>
|
||||
payload.findByID({
|
||||
id: first.id,
|
||||
collection,
|
||||
// omitting req for isolation
|
||||
}),
|
||||
).rejects.toThrow('The requested resource was not found.')
|
||||
|
||||
const second = await payload.create({
|
||||
collection,
|
||||
@@ -180,15 +275,13 @@ 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,
|
||||
collection,
|
||||
req,
|
||||
}),
|
||||
).rejects.toThrow('The requested resource was not found.')
|
||||
}
|
||||
await expect(() =>
|
||||
payload.findByID({
|
||||
id: first.id,
|
||||
collection,
|
||||
req,
|
||||
}),
|
||||
).rejects.toThrow('The requested resource was not found.')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 } },
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
14
test/helpers/setTestEnvPaths.ts
Normal file
14
test/helpers/setTestEnvPaths.ts
Normal 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
|
||||
}
|
||||
12
test/hooks/collections/Users/afterLoginHook.ts
Normal file
12
test/hooks/collections/Users/afterLoginHook.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -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],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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 } },
|
||||
})
|
||||
}
|
||||
|
||||
1
test/uploads/.gitignore
vendored
1
test/uploads/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
media
|
||||
uploads
|
||||
versions
|
||||
/media-gif
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,3 +9,5 @@ export const enlargeSlug = 'enlarge'
|
||||
export const reduceSlug = 'reduce'
|
||||
|
||||
export const adminThumbnailSlug = 'admin-thumbnail'
|
||||
|
||||
export const versionSlug = 'versions'
|
||||
|
||||
Reference in New Issue
Block a user