Compare commits

..

1 Commits

Author SHA1 Message Date
Dan Ribbens
9839655b07 fix: disableListFilter for array and group fields 2025-05-01 14:22:27 -04:00
102 changed files with 236 additions and 1278 deletions

View File

@@ -94,7 +94,6 @@ The Relationship Field inherits all of the default options from the base [Field
| **`allowCreate`** | Set to `false` if you'd like to disable the ability to create new documents from within the relationship field. |
| **`allowEdit`** | Set to `false` if you'd like to disable the ability to edit documents from within the relationship field. |
| **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More](#sort-options) |
| **`placeholder`** | Define a custom text or function to replace the generic default placeholder |
| **`appearance`** | Set to `drawer` or `select` to change the behavior of the field. Defaults to `select`. |
### Sort Options
@@ -150,7 +149,7 @@ The `filterOptions` property can either be a `Where` query, or a function return
| `id` | The `id` of the current document being edited. Will be `undefined` during the `create` operation or when called on a `Filter` component within the list view. |
| `relationTo` | The collection `slug` to filter against, limited to this field's `relationTo` property. |
| `req` | The Payload Request, which contains references to `payload`, `user`, `locale`, and more. |
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. Will be an empty object when called on a `Filter` component within the list view. |
| `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. Will be an emprt object when called on a `Filter` component within the list view. |
| `user` | An object containing the currently authenticated user. |
## Example

View File

@@ -89,7 +89,6 @@ The Select Field inherits all of the default options from the base [Field Admin
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **`isClearable`** | Set to `true` if you'd like this field to be clearable within the Admin UI. |
| **`isSortable`** | Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop. (Only works when `hasMany` is set to `true`) |
| **`placeholder`** | Define a custom text or function to replace the generic default placeholder |
## Example

View File

@@ -81,7 +81,7 @@ To install a Database Adapter, you can run **one** of the following commands:
#### 2. Copy Payload files into your Next.js app folder
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/(payload)) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](<https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/(payload)>) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
```plaintext
app/

View File

@@ -55,11 +55,10 @@ All collection `find` queries are paginated automatically. Responses are returne
All Payload APIs support the pagination controls below. With them, you can create paginated lists of documents within your application:
| Control | Default | Description |
| ------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `limit` | `10` | Limits the number of documents returned per page - set to `0` to show all documents, we automatically disabled pagination for you when `limit` is `0` for optimisation |
| `pagination` | `true` | Set to `false` to disable pagination and return all documents |
| `page` | `1` | Get a specific page number |
| Control | Description |
| ------- | --------------------------------------- |
| `limit` | Limits the number of documents returned |
| `page` | Get a specific page number |
### Disabling pagination within Local API

View File

@@ -84,7 +84,6 @@ pnpm add @payloadcms/storage-s3
- The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
- Configure `signedDownloads` (either globally of per-collection in `collections`) to use [presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html) for files downloading. This can improve performance for large files (like videos) while still respecting your access control.
```ts
import { s3Storage } from '@payloadcms/storage-s3'

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.37.0",
"version": "3.36.1",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/admin-bar",
"version": "3.37.0",
"version": "3.36.1",
"description": "An admin bar for React apps using Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.37.0",
"version": "3.36.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.37.0",
"version": "3.36.1",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.37.0",
"version": "3.36.1",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.37.0",
"version": "3.36.1",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
"version": "3.37.0",
"version": "3.36.1",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/drizzle",
"version": "3.37.0",
"version": "3.36.1",
"description": "A library of shared functions used by different payload database adapters",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -42,36 +42,33 @@ export const migrate: DrizzleAdapter['migrate'] = async function migrate(
limit: 0,
sort: '-name',
}))
if (migrationsInDB.find((m) => m.batch === -1)) {
const { confirm: runMigrations } = await prompts(
{
name: 'confirm',
type: 'confirm',
initial: false,
message:
"It looks like you've run Payload in dev mode, meaning you've dynamically pushed changes to your database.\n\n" +
"If you'd like to run migrations, data loss will occur. Would you like to proceed?",
},
{
onCancel: () => {
process.exit(0)
},
},
)
if (!runMigrations) {
process.exit(0)
}
// ignore the dev migration so that the latest batch number increments correctly
migrationsInDB = migrationsInDB.filter((m) => m.batch !== -1)
}
if (Number(migrationsInDB?.[0]?.batch) > 0) {
latestBatch = Number(migrationsInDB[0]?.batch)
}
}
if (migrationsInDB.find((m) => m.batch === -1)) {
const { confirm: runMigrations } = await prompts(
{
name: 'confirm',
type: 'confirm',
initial: false,
message:
"It looks like you've run Payload in dev mode, meaning you've dynamically pushed changes to your database.\n\n" +
"If you'd like to run migrations, data loss will occur. Would you like to proceed?",
},
{
onCancel: () => {
process.exit(0)
},
},
)
if (!runMigrations) {
process.exit(0)
}
}
const newBatch = latestBatch + 1
// Execute 'up' function for each migration sequentially

View File

@@ -1,18 +1,10 @@
export type Groups =
| 'addColumn'
| 'addConstraint'
| 'alterType'
| 'createIndex'
| 'createTable'
| 'createType'
| 'disableRowSecurity'
| 'dropColumn'
| 'dropConstraint'
| 'dropIndex'
| 'dropTable'
| 'dropType'
| 'notNull'
| 'setDefault'
/**
* Convert an "ADD COLUMN" statement to an "ALTER COLUMN" statement
@@ -52,35 +44,6 @@ export const groupUpSQLStatements = (list: string[]): Record<Groups, string[]> =
notNull: 'NOT NULL',
// example: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
createType: 'CREATE TYPE',
// example: CREATE TYPE "public"."enum__pages_v_published_locale" AS ENUM('en', 'es');
alterType: 'ALTER TYPE',
// example: ALTER TYPE "public"."enum_pages_blocks_cta" ADD VALUE 'copy';
createTable: 'CREATE TABLE',
// example: CREATE TABLE IF NOT EXISTS "payload_locked_documents" (
// "id" serial PRIMARY KEY NOT NULL,
// "global_slug" varchar,
// "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
// "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
// );
disableRowSecurity: 'DISABLE ROW LEVEL SECURITY;',
// example: ALTER TABLE "categories_rels" DISABLE ROW LEVEL SECURITY;
dropIndex: 'DROP INDEX IF EXISTS',
// example: DROP INDEX IF EXISTS "pages_title_idx";
setDefault: 'SET DEFAULT',
// example: ALTER TABLE "pages" ALTER COLUMN "_status" SET DEFAULT 'draft';
createIndex: 'INDEX IF NOT EXISTS',
// example: CREATE INDEX IF NOT EXISTS "payload_locked_documents_global_slug_idx" ON "payload_locked_documents" USING btree ("global_slug");
dropType: 'DROP TYPE',
// example: DROP TYPE "public"."enum__pages_v_published_locale";
}
const result = Object.keys(groups).reduce((result, group: Groups) => {
@@ -88,17 +51,7 @@ export const groupUpSQLStatements = (list: string[]): Record<Groups, string[]> =
return result
}, {}) as Record<Groups, string[]>
// push multi-line changes to a single grouping
let isCreateTable = false
for (const line of list) {
if (isCreateTable) {
result.createTable.push(line)
if (line.includes(');')) {
isCreateTable = false
}
continue
}
Object.entries(groups).some(([key, value]) => {
if (line.endsWith('NOT NULL;')) {
// split up the ADD COLUMN and ALTER COLUMN NOT NULL statements

View File

@@ -20,17 +20,6 @@ type Args = {
req?: Partial<PayloadRequest>
}
const runStatementGroup = async ({ adapter, db, debug, statements }) => {
const addColumnsStatement = statements.join('\n')
if (debug) {
adapter.payload.logger.info(debug)
adapter.payload.logger.info(addColumnsStatement)
}
await db.execute(sql.raw(addColumnsStatement))
}
/**
* Moves upload and relationship columns from the join table and into the tables while moving data
* This is done in the following order:
@@ -47,6 +36,7 @@ const runStatementGroup = async ({ adapter, db, debug, statements }) => {
*/
export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
const adapter = payload.db as unknown as BasePostgresAdapter
const db = await getTransaction(adapter, req)
const dir = payload.db.migrationDir
// get the drizzle migrateUpSQL from drizzle using the last schema
@@ -92,57 +82,14 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
const sqlUpStatements = groupUpSQLStatements(generatedSQL)
const db = await getTransaction(adapter, req)
const addColumnsStatement = sqlUpStatements.addColumn.join('\n')
await runStatementGroup({
adapter,
db,
debug: debug ? 'CREATING TYPES' : null,
statements: sqlUpStatements.createType,
})
if (debug) {
payload.logger.info('CREATING NEW RELATIONSHIP COLUMNS')
payload.logger.info(addColumnsStatement)
}
await runStatementGroup({
adapter,
db,
debug: debug ? 'ALTERING TYPES' : null,
statements: sqlUpStatements.alterType,
})
await runStatementGroup({
adapter,
db,
debug: debug ? 'CREATING TABLES' : null,
statements: sqlUpStatements.createTable,
})
await runStatementGroup({
adapter,
db,
debug: debug ? 'DISABLING ROW LEVEL SECURITY' : null,
statements: sqlUpStatements.disableRowSecurity,
})
await runStatementGroup({
adapter,
db,
debug: debug ? 'CREATING NEW RELATIONSHIP COLUMNS' : null,
statements: sqlUpStatements.addColumn,
})
// SET DEFAULTS
await runStatementGroup({
adapter,
db,
debug: debug ? 'SETTING DEFAULTS' : null,
statements: sqlUpStatements.setDefault,
})
await runStatementGroup({
adapter,
db,
debug: debug ? 'CREATING INDEXES' : null,
statements: sqlUpStatements.createIndex,
})
await db.execute(sql.raw(addColumnsStatement))
for (const collection of payload.config.collections) {
const tableName = adapter.tableNameMap.get(toSnakeCase(collection.slug))
@@ -290,58 +237,52 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
}
// ADD CONSTRAINT
await runStatementGroup({
adapter,
db,
debug: debug ? 'ADDING CONSTRAINTS' : null,
statements: sqlUpStatements.addConstraint,
})
const addConstraintsStatement = sqlUpStatements.addConstraint.join('\n')
if (debug) {
payload.logger.info('ADDING CONSTRAINTS')
payload.logger.info(addConstraintsStatement)
}
await db.execute(sql.raw(addConstraintsStatement))
// NOT NULL
await runStatementGroup({
adapter,
db,
debug: debug ? 'NOT NULL CONSTRAINTS' : null,
statements: sqlUpStatements.notNull,
})
const notNullStatements = sqlUpStatements.notNull.join('\n')
if (debug) {
payload.logger.info('NOT NULL CONSTRAINTS')
payload.logger.info(notNullStatements)
}
await db.execute(sql.raw(notNullStatements))
// DROP TABLE
await runStatementGroup({
adapter,
db,
debug: debug ? 'DROPPING TABLES' : null,
statements: sqlUpStatements.dropTable,
})
const dropTablesStatement = sqlUpStatements.dropTable.join('\n')
// DROP INDEX
await runStatementGroup({
adapter,
db,
debug: debug ? 'DROPPING INDEXES' : null,
statements: sqlUpStatements.dropIndex,
})
if (debug) {
payload.logger.info('DROPPING TABLES')
payload.logger.info(dropTablesStatement)
}
await db.execute(sql.raw(dropTablesStatement))
// DROP CONSTRAINT
await runStatementGroup({
adapter,
db,
debug: debug ? 'DROPPING CONSTRAINTS' : null,
statements: sqlUpStatements.dropConstraint,
})
const dropConstraintsStatement = sqlUpStatements.dropConstraint.join('\n')
if (debug) {
payload.logger.info('DROPPING CONSTRAINTS')
payload.logger.info(dropConstraintsStatement)
}
await db.execute(sql.raw(dropConstraintsStatement))
// DROP COLUMN
await runStatementGroup({
adapter,
db,
debug: debug ? 'DROPPING COLUMNS' : null,
statements: sqlUpStatements.dropColumn,
})
const dropColumnsStatement = sqlUpStatements.dropColumn.join('\n')
// DROP TYPES
await runStatementGroup({
adapter,
db,
debug: debug ? 'DROPPING TYPES' : null,
statements: sqlUpStatements.dropType,
})
if (debug) {
payload.logger.info('DROPPING COLUMNS')
payload.logger.info(dropColumnsStatement)
}
await db.execute(sql.raw(dropColumnsStatement))
}

View File

@@ -56,7 +56,7 @@ export const migrateRelationships = async ({
${where} ORDER BY parent_id LIMIT 500 OFFSET ${offset * 500};
`
paginationResult = await db.execute(sql.raw(`${paginationStatement}`))
paginationResult = await adapter.drizzle.execute(sql.raw(`${paginationStatement}`))
if (paginationResult.rows.length === 0) {
return
@@ -72,7 +72,7 @@ export const migrateRelationships = async ({
payload.logger.info(statement)
}
const result = await db.execute(sql.raw(`${statement}`))
const result = await adapter.drizzle.execute(sql.raw(`${statement}`))
const docsToResave: DocsToResave = {}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.37.0",
"version": "3.36.1",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-resend",
"version": "3.37.0",
"version": "3.36.1",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.37.0",
"version": "3.36.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -10,11 +10,11 @@ export const buildPaginatedListType = (name, docType) =>
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
hasPrevPage: { type: new GraphQLNonNull(GraphQLBoolean) },
limit: { type: new GraphQLNonNull(GraphQLInt) },
nextPage: { type: GraphQLInt },
nextPage: { type: new GraphQLNonNull(GraphQLInt) },
offset: { type: GraphQLInt },
page: { type: new GraphQLNonNull(GraphQLInt) },
pagingCounter: { type: new GraphQLNonNull(GraphQLInt) },
prevPage: { type: GraphQLInt },
prevPage: { type: new GraphQLNonNull(GraphQLInt) },
totalDocs: { type: new GraphQLNonNull(GraphQLInt) },
totalPages: { type: new GraphQLNonNull(GraphQLInt) },
},

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.37.0",
"version": "3.36.1",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -7,7 +7,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
// To prevent the flicker of stale data while the post message is being sent,
// you can conditionally render loading UI based on the `isLoading` state
export const useLivePreview = <T extends Record<string, unknown>>(props: {
export const useLivePreview = <T extends any>(props: {
apiRoute?: string
depth?: number
initialData: T
@@ -21,7 +21,7 @@ export const useLivePreview = <T extends Record<string, unknown>>(props: {
const [isLoading, setIsLoading] = useState<boolean>(true)
const hasSentReadyMessage = useRef<boolean>(false)
const onChange = useCallback((mergedData: T) => {
const onChange = useCallback((mergedData) => {
setData(mergedData)
setIsLoading(false)
}, [])

View File

@@ -1,4 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [{ "path": "../payload" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.37.0",
"version": "3.36.1",
"description": "The official Vue SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -8,7 +8,7 @@ import { onMounted, onUnmounted, ref } from 'vue'
*
* {@link https://payloadcms.com/docs/live-preview/frontend View the documentation}
*/
export const useLivePreview = <T extends Record<string, unknown>>(props: {
export const useLivePreview = <T>(props: {
apiRoute?: string
depth?: number
initialData: T
@@ -27,7 +27,7 @@ export const useLivePreview = <T extends Record<string, unknown>>(props: {
isLoading.value = false
}
let subscription: (event: MessageEvent) => Promise<void> | void
let subscription: (event: MessageEvent) => void
onMounted(() => {
subscription = subscribe({

View File

@@ -1,4 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
"noUncheckedIndexedAccess": false,
},
"references": [{ "path": "../payload" }] // db-mongodb depends on payload
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "3.37.0",
"version": "3.36.1",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.37.0",
"version": "3.36.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -2,14 +2,7 @@
import type { PaginatedDocs, Where } from 'payload'
import {
fieldBaseClass,
Pill,
ReactSelect,
useConfig,
useDocumentInfo,
useTranslation,
} from '@payloadcms/ui'
import { fieldBaseClass, Pill, ReactSelect, useConfig, useTranslation } from '@payloadcms/ui'
import { formatDate } from '@payloadcms/ui/shared'
import { stringify } from 'qs-esm'
import React, { useCallback, useEffect, useState } from 'react'
@@ -44,8 +37,6 @@ export const SelectComparison: React.FC<Props> = (props) => {
},
} = useConfig()
const { hasPublishedDoc } = useDocumentInfo()
const [options, setOptions] = useState<
{
label: React.ReactNode | string
@@ -118,10 +109,7 @@ export const SelectComparison: React.FC<Props> = (props) => {
},
published: {
currentLabel: t('version:currentPublishedVersion'),
// The latest published version does not necessarily equal the current published version,
// because the latest published version might have been unpublished in the meantime.
// Hence, we should only use the latest published version if there is a published document.
latestVersion: hasPublishedDoc ? latestPublishedVersion : undefined,
latestVersion: latestPublishedVersion,
pillStyle: 'success',
previousLabel: t('version:previouslyPublished'),
},

View File

@@ -85,34 +85,13 @@ export async function VersionsView(props: DocumentViewServerProps) {
payload,
status: 'draft',
})
const publishedDoc = await payload.count({
collection: collectionSlug,
depth: 0,
overrideAccess: true,
req,
where: {
id: {
equals: id,
},
_status: {
equals: 'published',
},
},
latestPublishedVersion = await getLatestVersion({
slug: collectionSlug,
type: 'collection',
parentID: id,
payload,
status: 'published',
})
// If we pass a latestPublishedVersion to buildVersionColumns,
// this will be used to display it as the "current published version".
// However, the latest published version might have been unpublished in the meantime.
// Hence, we should only pass the latest published version if there is a published document.
latestPublishedVersion =
publishedDoc.totalDocs > 0 &&
(await getLatestVersion({
slug: collectionSlug,
type: 'collection',
parentID: id,
payload,
status: 'published',
}))
}
} catch (err) {
logError({ err, payload })

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/payload-cloud",
"version": "3.37.0",
"version": "3.36.1",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.37.0",
"version": "3.36.1",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",

View File

@@ -0,0 +1,7 @@
const isLocked = (date: number): boolean => {
if (!date) {
return false
}
return date > Date.now()
}
export default isLocked

View File

@@ -1,6 +0,0 @@
export const isUserLocked = (date: number): boolean => {
if (!date) {
return false
}
return date > Date.now()
}

View File

@@ -3,7 +3,6 @@ import type {
AuthOperationsFromCollectionSlug,
Collection,
DataFromCollectionSlug,
SanitizedCollectionConfig,
} from '../../collections/config/types.js'
import type { CollectionSlug } from '../../index.js'
import type { PayloadRequest, Where } from '../../types/index.js'
@@ -22,7 +21,7 @@ import { killTransaction } from '../../utilities/killTransaction.js'
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js'
import { getFieldsToSign } from '../getFieldsToSign.js'
import { getLoginOptions } from '../getLoginOptions.js'
import { isUserLocked } from '../isUserLocked.js'
import isLocked from '../isLocked.js'
import { jwtSign } from '../jwt.js'
import { authenticateLocalStrategy } from '../strategies/local/authenticate.js'
import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts.js'
@@ -43,32 +42,6 @@ export type Arguments<TSlug extends CollectionSlug> = {
showHiddenFields?: boolean
}
type CheckLoginPermissionArgs = {
collection: SanitizedCollectionConfig
loggingInWithUsername?: boolean
req: PayloadRequest
user: any
}
export const checkLoginPermission = ({
collection,
loggingInWithUsername,
req,
user,
}: CheckLoginPermissionArgs) => {
if (!user) {
throw new AuthenticationError(req.t, Boolean(loggingInWithUsername))
}
if (collection.auth.verify && user._verified === false) {
throw new UnverifiedEmail({ t: req.t })
}
if (isUserLocked(new Date(user.lockUntil).getTime())) {
throw new LockedAuth(req.t)
}
}
export const loginOperation = async <TSlug extends CollectionSlug>(
incomingArgs: Arguments<TSlug>,
): Promise<{ user: DataFromCollectionSlug<TSlug> } & Result> => {
@@ -211,16 +184,21 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
where: whereConstraint,
})
checkLoginPermission({
collection: collectionConfig,
loggingInWithUsername: Boolean(canLoginWithUsername && sanitizedUsername),
req,
user,
})
if (!user) {
throw new AuthenticationError(req.t, Boolean(canLoginWithUsername && sanitizedUsername))
}
if (args.collection.config.auth.verify && user._verified === false) {
throw new UnverifiedEmail({ t: req.t })
}
user.collection = collectionConfig.slug
user._strategy = 'local-jwt'
if (isLocked(new Date(user.lockUntil).getTime())) {
throw new LockedAuth(req.t)
}
const authResult = await authenticateLocalStrategy({ doc: user, password })
user = sanitizeInternalFields(user)

View File

@@ -1061,7 +1061,6 @@ export type SelectField = {
} & Admin['components']
isClearable?: boolean
isSortable?: boolean
placeholder?: LabelFunction | string
} & Admin
/**
* Customize the SQL table name
@@ -1094,7 +1093,7 @@ export type SelectField = {
Omit<FieldBase, 'validate'>
export type SelectFieldClient = {
admin?: AdminClient & Pick<SelectField['admin'], 'isClearable' | 'isSortable' | 'placeholder'>
admin?: AdminClient & Pick<SelectField['admin'], 'isClearable' | 'isSortable'>
} & FieldBaseClient &
Pick<SelectField, 'hasMany' | 'interfaceName' | 'options' | 'type'>
@@ -1161,11 +1160,10 @@ type RelationshipAdmin = {
>
} & Admin['components']
isSortable?: boolean
placeholder?: LabelFunction | string
} & Admin
type RelationshipAdminClient = AdminClient &
Pick<RelationshipAdmin, 'allowCreate' | 'allowEdit' | 'appearance' | 'isSortable' | 'placeholder'>
Pick<RelationshipAdmin, 'allowCreate' | 'allowEdit' | 'appearance' | 'isSortable'>
export type PolymorphicRelationshipField = {
admin?: {

View File

@@ -89,10 +89,6 @@ import { traverseFields } from './utilities/traverseFields.js'
export { default as executeAccess } from './auth/executeAccess.js'
export { executeAuthStrategies } from './auth/executeAuthStrategies.js'
export { extractAccessFromPermission } from './auth/extractAccessFromPermission.js'
export { getAccessResults } from './auth/getAccessResults.js'
export { getFieldsToSign } from './auth/getFieldsToSign.js'
export { getLoginOptions } from './auth/getLoginOptions.js'
export interface GeneratedTypes {
authUntyped: {
@@ -981,12 +977,13 @@ interface RequestContext {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface DatabaseAdapter extends BaseDatabaseAdapter {}
export type { Payload, RequestContext }
export { extractAccessFromPermission } from './auth/extractAccessFromPermission.js'
export { getAccessResults } from './auth/getAccessResults.js'
export { getFieldsToSign } from './auth/getFieldsToSign.js'
export * from './auth/index.js'
export { jwtSign } from './auth/jwt.js'
export { accessOperation } from './auth/operations/access.js'
export { forgotPasswordOperation } from './auth/operations/forgotPassword.js'
export { initOperation } from './auth/operations/init.js'
export { checkLoginPermission } from './auth/operations/login.js'
export { loginOperation } from './auth/operations/login.js'
export { logoutOperation } from './auth/operations/logout.js'
export type { MeOperationResult } from './auth/operations/me.js'
@@ -997,8 +994,6 @@ export { resetPasswordOperation } from './auth/operations/resetPassword.js'
export { unlockOperation } from './auth/operations/unlock.js'
export { verifyEmailOperation } from './auth/operations/verifyEmail.js'
export { JWTAuthentication } from './auth/strategies/jwt.js'
export { incrementLoginAttempts } from './auth/strategies/local/incrementLoginAttempts.js'
export { resetLoginAttempts } from './auth/strategies/local/resetLoginAttempts.js'
export type {
AuthStrategyFunction,
AuthStrategyFunctionArgs,
@@ -1206,7 +1201,6 @@ export {
MissingFile,
NotFound,
QueryError,
UnverifiedEmail,
ValidationError,
ValidationErrorName,
} from './errors/index.js'

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud-storage",
"version": "3.37.0",
"version": "3.36.1",
"description": "The official cloud storage plugin for Payload CMS",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -26,7 +26,6 @@ export async function getFilePrefix({
const files = await req.payload.find({
collection: collection.slug,
depth: 0,
draft: true,
limit: 1,
pagination: false,
where: {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
"version": "3.37.0",
"version": "3.36.1",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-import-export",
"version": "3.37.0",
"version": "3.36.1",
"description": "Import-Export plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-multi-tenant",
"version": "3.37.0",
"version": "3.36.1",
"description": "Multi Tenant plugin for Payload",
"keywords": [
"payload",

View File

@@ -14,7 +14,6 @@ export const findTenantOptions = async ({
useAsTitle,
user,
}: Args): Promise<PaginatedDocs> => {
const isOrderable = payload.collections[tenantsCollectionSlug]?.config?.orderable || false
return payload.find({
collection: tenantsCollectionSlug,
depth: 0,
@@ -22,9 +21,8 @@ export const findTenantOptions = async ({
overrideAccess: false,
select: {
[useAsTitle]: true,
...(isOrderable ? { _order: true } : {}),
},
sort: isOrderable ? '_order' : useAsTitle,
sort: useAsTitle,
user,
})
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
"version": "3.37.0",
"version": "3.36.1",
"description": "The official Nested Docs plugin for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-redirects",
"version": "3.37.0",
"version": "3.36.1",
"description": "Redirects plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-search",
"version": "3.37.0",
"version": "3.36.1",
"description": "Search plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-sentry",
"version": "3.37.0",
"version": "3.36.1",
"description": "Sentry plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-seo",
"version": "3.37.0",
"version": "3.36.1",
"description": "SEO plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-stripe",
"version": "3.37.0",
"version": "3.36.1",
"description": "Stripe plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "3.37.0",
"version": "3.36.1",
"description": "The officially supported Lexical richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -5,12 +5,6 @@ import type {
SerializedParagraphNode,
SerializedTextNode,
SerializedLineBreakNode,
SerializedHeadingNode,
SerializedListItemNode,
SerializedListNode,
SerializedTableRowNode,
SerializedTableNode,
SerializedTableCellNode,
} from '../../../nodeTypes.js'
import { convertLexicalToPlaintext } from './sync/index.js'
@@ -57,83 +51,7 @@ function paragraphNode(children: DefaultNodeTypes[]): SerializedParagraphNode {
}
}
function headingNode(children: DefaultNodeTypes[]): SerializedHeadingNode {
return {
type: 'heading',
children,
direction: 'ltr',
format: '',
indent: 0,
textFormat: 0,
tag: 'h1',
version: 1,
}
}
function listItemNode(children: DefaultNodeTypes[]): SerializedListItemNode {
return {
type: 'listitem',
children,
checked: false,
direction: 'ltr',
format: '',
indent: 0,
value: 0,
version: 1,
}
}
function listNode(children: DefaultNodeTypes[]): SerializedListNode {
return {
type: 'list',
children,
direction: 'ltr',
format: '',
indent: 0,
listType: 'bullet',
start: 0,
tag: 'ul',
version: 1,
}
}
function tableNode(children: (DefaultNodeTypes | SerializedTableRowNode)[]): SerializedTableNode {
return {
type: 'table',
children,
direction: 'ltr',
format: '',
indent: 0,
version: 1,
}
}
function tableRowNode(
children: (DefaultNodeTypes | SerializedTableCellNode)[],
): SerializedTableRowNode {
return {
type: 'tablerow',
children,
direction: 'ltr',
format: '',
indent: 0,
version: 1,
}
}
function tableCellNode(children: DefaultNodeTypes[]): SerializedTableCellNode {
return {
type: 'tablecell',
children,
direction: 'ltr',
format: '',
indent: 0,
headerState: 0,
version: 1,
}
}
function rootNode(nodes: (DefaultNodeTypes | SerializedTableNode)[]): DefaultTypedEditorState {
function rootNode(nodes: DefaultNodeTypes[]): DefaultTypedEditorState {
return {
root: {
type: 'root',
@@ -154,6 +72,7 @@ describe('convertLexicalToPlaintext', () => {
data,
})
console.log('plaintext', plaintext)
expect(plaintext).toBe('Basic Text')
})
@@ -192,67 +111,4 @@ describe('convertLexicalToPlaintext', () => {
expect(plaintext).toBe('Basic Text\tNext Line')
})
it('ensure new lines are added between paragraphs', () => {
const data: DefaultTypedEditorState = rootNode([
paragraphNode([textNode('Basic text')]),
paragraphNode([textNode('Next block-node')]),
])
const plaintext = convertLexicalToPlaintext({
data,
})
expect(plaintext).toBe('Basic text\n\nNext block-node')
})
it('ensure new lines are added between heading nodes', () => {
const data: DefaultTypedEditorState = rootNode([
headingNode([textNode('Basic text')]),
headingNode([textNode('Next block-node')]),
])
const plaintext = convertLexicalToPlaintext({
data,
})
expect(plaintext).toBe('Basic text\n\nNext block-node')
})
it('ensure new lines are added between list items and lists', () => {
const data: DefaultTypedEditorState = rootNode([
listNode([listItemNode([textNode('First item')]), listItemNode([textNode('Second item')])]),
listNode([listItemNode([textNode('Next list')])]),
])
const plaintext = convertLexicalToPlaintext({
data,
})
expect(plaintext).toBe('First item\nSecond item\n\nNext list')
})
it('ensure new lines are added between tables, table rows, and table cells', () => {
const data: DefaultTypedEditorState = rootNode([
tableNode([
tableRowNode([
tableCellNode([textNode('Cell 1, Row 1')]),
tableCellNode([textNode('Cell 2, Row 1')]),
]),
tableRowNode([
tableCellNode([textNode('Cell 1, Row 2')]),
tableCellNode([textNode('Cell 2, Row 2')]),
]),
]),
tableNode([tableRowNode([tableCellNode([textNode('Cell in Table 2')])])]),
])
const plaintext = convertLexicalToPlaintext({
data,
})
expect(plaintext).toBe(
'Cell 1, Row 1 | Cell 2, Row 1\nCell 1, Row 2 | Cell 2, Row 2\n\nCell in Table 2',
)
})
})

View File

@@ -86,25 +86,11 @@ export function convertLexicalNodesToPlaintext({
}
} else {
// Default plaintext converter heuristic
if (
node.type === 'paragraph' ||
node.type === 'heading' ||
node.type === 'list' ||
node.type === 'table'
) {
if (node.type === 'paragraph') {
if (plainTextArray?.length) {
// Only add a new line if there is already text in the array
plainTextArray.push('\n\n')
}
} else if (node.type === 'listitem' || node.type === 'tablerow') {
if (plainTextArray?.length) {
// Only add a new line if there is already text in the array
plainTextArray.push('\n')
}
} else if (node.type === 'tablecell') {
if (plainTextArray?.length) {
plainTextArray.push(' | ')
}
} else if (node.type === 'linebreak') {
plainTextArray.push('\n')
} else if (node.type === 'tab') {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-slate",
"version": "3.37.0",
"version": "3.36.1",
"description": "The officially supported Slate richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-azure",
"version": "3.37.0",
"version": "3.36.1",
"description": "Payload storage adapter for Azure Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-gcs",
"version": "3.37.0",
"version": "3.36.1",
"description": "Payload storage adapter for Google Cloud Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-s3",
"version": "3.37.0",
"version": "3.36.1",
"description": "Payload storage adapter for Amazon S3",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -12,8 +12,6 @@ import * as AWS from '@aws-sdk/client-s3'
import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'
import { initClientUploads } from '@payloadcms/plugin-cloud-storage/utilities'
import type { SignedDownloadsConfig } from './staticHandler.js'
import { getGenerateSignedURLHandler } from './generateSignedURL.js'
import { getGenerateURL } from './generateURL.js'
import { getHandleDelete } from './handleDelete.js'
@@ -26,7 +24,6 @@ export type S3StorageOptions = {
*/
acl?: 'private' | 'public-read'
/**
* Bucket name to upload files to.
*
@@ -42,15 +39,8 @@ export type S3StorageOptions = {
/**
* Collection options to apply the S3 adapter to.
*/
collections: Partial<
Record<
UploadCollectionSlug,
| ({
signedDownloads?: SignedDownloadsConfig
} & Omit<CollectionOptions, 'adapter'>)
| true
>
>
collections: Partial<Record<UploadCollectionSlug, Omit<CollectionOptions, 'adapter'> | true>>
/**
* AWS S3 client configuration. Highly dependent on your AWS setup.
*
@@ -71,10 +61,6 @@ export type S3StorageOptions = {
* Default: true
*/
enabled?: boolean
/**
* Use pre-signed URLs for files downloading. Can be overriden per-collection.
*/
signedDownloads?: SignedDownloadsConfig
}
type S3StoragePlugin = (storageS3Args: S3StorageOptions) => Plugin
@@ -172,27 +158,9 @@ export const s3Storage: S3StoragePlugin =
function s3StorageInternal(
getStorageClient: () => AWS.S3,
{
acl,
bucket,
clientUploads,
collections,
config = {},
signedDownloads: topLevelSignedDownloads,
}: S3StorageOptions,
{ acl, bucket, clientUploads, config = {} }: S3StorageOptions,
): Adapter {
return ({ collection, prefix }): GeneratedAdapter => {
const collectionStorageConfig = collections[collection.slug]
let signedDownloads: null | SignedDownloadsConfig =
typeof collectionStorageConfig === 'object'
? (collectionStorageConfig.signedDownloads ?? false)
: null
if (signedDownloads === null) {
signedDownloads = topLevelSignedDownloads ?? null
}
return {
name: 's3',
clientUploads,
@@ -205,12 +173,7 @@ function s3StorageInternal(
getStorageClient,
prefix,
}),
staticHandler: getHandler({
bucket,
collection,
getStorageClient,
signedDownloads: signedDownloads ?? false,
}),
staticHandler: getHandler({ bucket, collection, getStorageClient }),
}
}
}

View File

@@ -3,23 +3,13 @@ import type { StaticHandler } from '@payloadcms/plugin-cloud-storage/types'
import type { CollectionConfig } from 'payload'
import type { Readable } from 'stream'
import { GetObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { getFilePrefix } from '@payloadcms/plugin-cloud-storage/utilities'
import path from 'path'
export type SignedDownloadsConfig =
| {
/** @default 7200 */
expiresIn?: number
}
| boolean
interface Args {
bucket: string
collection: CollectionConfig
getStorageClient: () => AWS.S3
signedDownloads?: SignedDownloadsConfig
}
// Type guard for NodeJS.Readable streams
@@ -50,12 +40,7 @@ const streamToBuffer = async (readableStream: any) => {
return Buffer.concat(chunks)
}
export const getHandler = ({
bucket,
collection,
getStorageClient,
signedDownloads,
}: Args): StaticHandler => {
export const getHandler = ({ bucket, collection, getStorageClient }: Args): StaticHandler => {
return async (req, { params: { clientUploadContext, filename } }) => {
let object: AWS.GetObjectOutput | undefined = undefined
try {
@@ -63,17 +48,6 @@ export const getHandler = ({
const key = path.posix.join(prefix, filename)
if (signedDownloads && !clientUploadContext) {
const command = new GetObjectCommand({ Bucket: bucket, Key: key })
const signedUrl = await getSignedUrl(
// @ts-expect-error mismatch versions
getStorageClient(),
command,
typeof signedDownloads === 'object' ? signedDownloads : { expiresIn: 7200 },
)
return Response.redirect(signedUrl)
}
object = await getStorageClient().getObject({
Bucket: bucket,
Key: key,

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-uploadthing",
"version": "3.37.0",
"version": "3.36.1",
"description": "Payload storage adapter for uploadthing",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -13,6 +13,6 @@ export const UploadthingClientUploadHandler = createClientUploadHandler({
files: [file],
})
return { key: res[0]?.key }
return { key: res[0].key }
},
})

View File

@@ -33,7 +33,7 @@ export const getHandler = ({ utApi }: Args): StaticHandler => {
},
]
if (collectionConfig?.upload.imageSizes) {
if (collectionConfig.upload.imageSizes) {
collectionConfig.upload.imageSizes.forEach(({ name }) => {
or.push({
[`sizes.${name}.filename`]: {
@@ -58,7 +58,7 @@ export const getHandler = ({ utApi }: Args): StaticHandler => {
return new Response(null, { status: 404, statusText: 'Not Found' })
}
key = getKeyFromFilename(retrievedDoc, filename)!
key = getKeyFromFilename(retrievedDoc, filename)
}
if (!key) {
@@ -97,7 +97,7 @@ export const getHandler = ({ utApi }: Args): StaticHandler => {
headers: new Headers({
'Content-Length': String(blob.size),
'Content-Type': blob.type,
ETag: objectEtag!,
ETag: objectEtag,
}),
status: 200,
})

View File

@@ -1,4 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
/* TODO: remove the following lines */
"strict": false,
},
"references": [{ "path": "../payload" }, { "path": "../plugin-cloud-storage" }]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-vercel-blob",
"version": "3.37.0",
"version": "3.36.1",
"description": "Payload storage adapter for Vercel Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/translations",
"version": "3.37.0",
"version": "3.36.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/ui",
"version": "3.37.0",
"version": "3.36.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -2,12 +2,7 @@
import React from 'react'
import './index.scss'
import { Link } from '../../elements/Link/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { sanitizeID } from '../../utilities/sanitizeID.js'
import { useDrawerDepth } from '../Drawer/index.js'
const baseClass = 'id-label'
@@ -15,26 +10,10 @@ export const IDLabel: React.FC<{ className?: string; id: string; prefix?: string
id,
className,
prefix = 'ID:',
}) => {
const {
config: {
routes: { admin: adminRoute },
},
} = useConfig()
const { collectionSlug, globalSlug } = useDocumentInfo()
const drawerDepth = useDrawerDepth()
const docPath = formatAdminURL({
adminRoute,
path: `/${collectionSlug ? `collections/${collectionSlug}` : `globals/${globalSlug}`}/${id}`,
})
return (
<div className={[baseClass, className].filter(Boolean).join(' ')} title={id}>
{prefix}
&nbsp;
{drawerDepth > 1 ? <Link href={docPath}>{sanitizeID(id)}</Link> : sanitizeID(id)}
</div>
)
}
}) => (
<div className={[baseClass, className].filter(Boolean).join(' ')} title={id}>
{prefix}
&nbsp;
{sanitizeID(id)}
</div>
)

View File

@@ -84,6 +84,7 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
captureMenuScroll
customProps={customProps}
isLoading={isLoading}
placeholder={getTranslation(placeholder, i18n)}
{...props}
className={classes}
classNamePrefix="rs"
@@ -112,7 +113,6 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
onMenuClose={onMenuClose}
onMenuOpen={onMenuOpen}
options={options}
placeholder={getTranslation(placeholder, i18n)}
styles={styles}
unstyled={true}
value={value}
@@ -160,6 +160,7 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
<CreatableSelect
captureMenuScroll
isLoading={isLoading}
placeholder={getTranslation(placeholder, i18n)}
{...props}
className={classes}
classNamePrefix="rs"
@@ -190,7 +191,6 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
onMenuClose={onMenuClose}
onMenuOpen={onMenuOpen}
options={options}
placeholder={getTranslation(placeholder, i18n)}
styles={styles}
unstyled={true}
value={value}

View File

@@ -1,11 +1,11 @@
import type { LabelFunction } from 'payload'
import type { CommonProps, GroupBase, Props as ReactSelectStateManagerProps } from 'react-select'
import type { DocumentDrawerProps } from '../DocumentDrawer/types.js'
import type { DocumentDrawerProps, UseDocumentDrawer } from '../DocumentDrawer/types.js'
type CustomSelectProps = {
disableKeyDown?: boolean
disableMouseDown?: boolean
DocumentDrawerToggler?: ReturnType<UseDocumentDrawer>[1]
draggableProps?: any
droppableRef?: React.RefObject<HTMLDivElement | null>
editableProps?: (
@@ -14,11 +14,10 @@ type CustomSelectProps = {
selectProps: ReactSelectStateManagerProps,
) => any
onDelete?: DocumentDrawerProps['onDelete']
onDocumentOpen?: (args: {
onDocumentDrawerOpen?: (args: {
collectionSlug: string
hasReadPermission: boolean
id: number | string
openInNewTab?: boolean
}) => void
onDuplicate?: DocumentDrawerProps['onSave']
onSave?: DocumentDrawerProps['onSave']
@@ -102,7 +101,7 @@ export type ReactSelectAdapterProps = {
onMenuOpen?: () => void
onMenuScrollToBottom?: () => void
options: Option[] | OptionGroup[]
placeholder?: LabelFunction | string
placeholder?: string
showError?: boolean
value?: Option | Option[]
}

View File

@@ -117,19 +117,7 @@ export const Status: React.FC = () => {
setUnpublishedVersionCount(0)
}
} else {
try {
const json = await res.json()
if (json.errors?.[0]?.message) {
toast.error(json.errors[0].message)
} else if (json.error) {
toast.error(json.error)
} else {
toast.error(t('error:unPublishingDocument'))
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {
toast.error(t('error:unPublishingDocument'))
}
toast.error(t('error:unPublishingDocument'))
}
},
[
@@ -166,7 +154,6 @@ export const Status: React.FC = () => {
<Button
buttonStyle="none"
className={`${baseClass}__action`}
id={`action-unpublish`}
onClick={() => toggleModal(unPublishModalSlug)}
>
{t('version:unpublish')}

View File

@@ -23,7 +23,7 @@ const maxResultsPerRequest = 10
export const RelationshipFilter: React.FC<Props> = (props) => {
const {
disabled,
field: { admin: { isSortable, placeholder } = {}, hasMany, relationTo },
field: { admin: { isSortable } = {}, hasMany, relationTo },
filterOptions,
onChange,
value,
@@ -324,7 +324,7 @@ export const RelationshipFilter: React.FC<Props> = (props) => {
}
})
}
}, [i18n, relationTo, debouncedSearch, filterOptions])
}, [i18n, relationTo, debouncedSearch])
/**
* Load any other options that might exist in the value that were not loaded already
@@ -412,7 +412,7 @@ export const RelationshipFilter: React.FC<Props> = (props) => {
onInputChange={handleInputChange}
onMenuScrollToBottom={handleScrollToBottom}
options={options}
placeholder={placeholder}
placeholder={t('general:selectValue')}
value={valueToRender}
/>
)}

View File

@@ -11,9 +11,6 @@ import { formatOptions } from './formatOptions.js'
export const Select: React.FC<Props> = ({
disabled,
field: {
admin: { placeholder },
},
isClearable,
onChange,
operator,
@@ -80,7 +77,6 @@ export const Select: React.FC<Props> = ({
isMulti={isMulti}
onChange={onSelect}
options={options.map((option) => ({ ...option, label: getTranslation(option.label, i18n) }))}
placeholder={placeholder}
value={valueToRender}
/>
)

View File

@@ -1,4 +1,4 @@
import type { LabelFunction, Option, SelectFieldClient } from 'payload'
import type { Option, SelectFieldClient } from 'payload'
import type { DefaultFilterProps } from '../types.js'
@@ -7,6 +7,5 @@ export type SelectFilterProps = {
readonly isClearable?: boolean
readonly onChange: (val: string) => void
readonly options: Option[]
readonly placeholder?: LabelFunction | string
readonly value: string
} & DefaultFilterProps

View File

@@ -141,11 +141,6 @@ export const Condition: React.FC<Props> = (props) => {
<div className={`${baseClass}__field`}>
<ReactSelect
disabled={disabled}
filterOption={(option, inputValue) =>
((option?.data?.plainTextLabel as string) || option.label)
.toLowerCase()
.includes(inputValue.toLowerCase())
}
isClearable={false}
onChange={handleFieldChange}
options={reducedFields.filter((field) => !field.field.admin.disableListFilter)}

View File

@@ -4,7 +4,6 @@ import type { ClientField } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { fieldIsHiddenOrDisabled, fieldIsID, tabHasName } from 'payload/shared'
import { renderToStaticMarkup } from 'react-dom/server'
import type { ReducedField } from './types.js'
@@ -100,7 +99,11 @@ export const reduceFields = ({
return reduced
}
if ((field.type === 'group' || field.type === 'array') && 'fields' in field) {
if (
(field.type === 'group' || field.type === 'array') &&
'fields' in field &&
!field.admin.disableListFilter
) {
const translatedLabel = getTranslation(field.label || '', i18n)
const labelWithPrefix = labelPrefix
@@ -153,15 +156,10 @@ export const reduceFields = ({
})
: localizedLabel
// React elements in filter options are not searchable in React Select
// Extract plain text to make them filterable in dropdowns
const textFromLabel = extractTextFromReactNode(formattedLabel)
const fieldPath = pathPrefix ? createNestedClientFieldPath(pathPrefix, field) : field.name
const formattedField: ReducedField = {
label: formattedLabel,
plainTextLabel: textFromLabel,
value: fieldPath,
...fieldTypes[field.type],
field,
@@ -174,29 +172,3 @@ export const reduceFields = ({
return reduced
}, [])
}
/**
* Extracts plain text content from a React node by removing HTML tags.
* Used to make React elements searchable in filter dropdowns.
*/
const extractTextFromReactNode = (reactNode: React.ReactNode): string => {
if (!reactNode) {
return ''
}
if (typeof reactNode === 'string') {
return reactNode
}
const html = renderToStaticMarkup(reactNode)
// Handle different environments (server vs browser)
if (typeof document !== 'undefined') {
// Browser environment - use actual DOM
const div = document.createElement('div')
div.innerHTML = html
return div.textContent || ''
} else {
// Server environment - use regex to strip HTML tags
return html.replace(/<[^>]*>/g, '')
}
}

View File

@@ -23,7 +23,6 @@ export type ReducedField = {
label: string
value: Operator
}[]
plainTextLabel?: string
value: Value
}

View File

@@ -7,9 +7,9 @@ import type {
} from 'payload'
import { dequal } from 'dequal/lite'
import { formatAdminURL, wordBoundariesRegex } from 'payload/shared'
import { wordBoundariesRegex } from 'payload/shared'
import * as qs from 'qs-esm'
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import type { DocumentDrawerProps } from '../../elements/DocumentDrawer/types.js'
import type { ListDrawerProps } from '../../elements/ListDrawer/types.js'
@@ -56,7 +56,6 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
className,
description,
isSortable = true,
placeholder,
sortOptions,
} = {},
hasMany,
@@ -83,7 +82,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
const hasMultipleRelations = Array.isArray(relationTo)
const [currentlyOpenRelationship, setCurrentlyOpenRelationship] = useState<
Parameters<ReactSelectAdapterProps['customProps']['onDocumentOpen']>[0]
Parameters<ReactSelectAdapterProps['customProps']['onDocumentDrawerOpen']>[0]
>({
id: undefined,
collectionSlug: undefined,
@@ -631,29 +630,16 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
return r.test(labelString.slice(-breakApartThreshold))
}, [])
const onDocumentOpen = useCallback<ReactSelectAdapterProps['customProps']['onDocumentOpen']>(
({ id, collectionSlug, hasReadPermission, openInNewTab }) => {
if (openInNewTab) {
if (hasReadPermission && id && collectionSlug) {
const docUrl = formatAdminURL({
adminRoute: config.routes.admin,
path: `/collections/${collectionSlug}/${id}`,
})
window.open(docUrl, '_blank')
}
} else {
openDrawerWhenRelationChanges.current = true
setCurrentlyOpenRelationship({
id,
collectionSlug,
hasReadPermission,
})
}
},
[setCurrentlyOpenRelationship, config.routes.admin],
)
const onDocumentDrawerOpen = useCallback<
ReactSelectAdapterProps['customProps']['onDocumentDrawerOpen']
>(({ id, collectionSlug, hasReadPermission }) => {
openDrawerWhenRelationChanges.current = true
setCurrentlyOpenRelationship({
id,
collectionSlug,
hasReadPermission,
})
}, [])
useEffect(() => {
if (openDrawerWhenRelationChanges.current) {
@@ -710,7 +696,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
customProps={{
disableKeyDown: isDrawerOpen || isListDrawerOpen,
disableMouseDown: isDrawerOpen || isListDrawerOpen,
onDocumentOpen,
onDocumentDrawerOpen,
onSave,
}}
disabled={readOnly || disabled || isDrawerOpen || isListDrawerOpen}
@@ -793,7 +779,6 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
})
}}
options={options}
placeholder={placeholder}
showError={showError}
value={valueToRender ?? null}
/>

View File

@@ -25,7 +25,7 @@ export const MultiValueLabel: React.FC<
> = (props) => {
const {
data: { allowEdit, label, relationTo, value },
selectProps: { customProps: { draggableProps, onDocumentOpen } = {} } = {},
selectProps: { customProps: { draggableProps, onDocumentDrawerOpen } = {} } = {},
} = props
const { permissions } = useAuth()
@@ -49,13 +49,12 @@ export const MultiValueLabel: React.FC<
<button
aria-label={`Edit ${label}`}
className={`${baseClass}__drawer-toggler`}
onClick={(event) => {
onClick={() => {
setShowTooltip(false)
onDocumentOpen({
onDocumentDrawerOpen({
id: value,
collectionSlug: relationTo,
hasReadPermission,
openInNewTab: event.metaKey || event.ctrlKey,
})
}}
onKeyDown={(e) => {

View File

@@ -26,7 +26,7 @@ export const SingleValue: React.FC<
const {
children,
data: { allowEdit, label, relationTo, value },
selectProps: { customProps: { onDocumentOpen } = {} } = {},
selectProps: { customProps: { onDocumentDrawerOpen } = {} } = {},
} = props
const [showTooltip, setShowTooltip] = useState(false)
@@ -44,13 +44,12 @@ export const SingleValue: React.FC<
<button
aria-label={t('general:editLabel', { label })}
className={`${baseClass}__drawer-toggler`}
onClick={(event) => {
onClick={() => {
setShowTooltip(false)
onDocumentOpen({
onDocumentDrawerOpen({
id: value,
collectionSlug: relationTo,
hasReadPermission,
openInNewTab: event.metaKey || event.ctrlKey,
})
}}
onKeyDown={(e) => {

View File

@@ -1,5 +1,5 @@
'use client'
import type { LabelFunction, OptionObject, StaticDescription, StaticLabel } from 'payload'
import type { OptionObject, StaticDescription, StaticLabel } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import React from 'react'
@@ -33,7 +33,6 @@ export type SelectInputProps = {
readonly onInputChange?: ReactSelectAdapterProps['onInputChange']
readonly options?: OptionObject[]
readonly path: string
readonly placeholder?: LabelFunction | string
readonly readOnly?: boolean
readonly required?: boolean
readonly showError?: boolean
@@ -59,7 +58,6 @@ export const SelectInput: React.FC<SelectInputProps> = (props) => {
onInputChange,
options,
path,
placeholder,
readOnly,
required,
showError,
@@ -127,7 +125,6 @@ export const SelectInput: React.FC<SelectInputProps> = (props) => {
...option,
label: getTranslation(option.label, i18n),
}))}
placeholder={placeholder}
showError={showError}
value={valueToRender as OptionObject}
/>

View File

@@ -38,7 +38,6 @@ const SelectFieldComponent: SelectFieldClientComponent = (props) => {
description,
isClearable = true,
isSortable = true,
placeholder,
} = {} as SelectFieldClientProps['field']['admin'],
hasMany = false,
label,
@@ -119,7 +118,6 @@ const SelectFieldComponent: SelectFieldClientComponent = (props) => {
onChange={onChange}
options={options}
path={path}
placeholder={placeholder}
readOnly={readOnly || disabled}
required={required}
showError={showError}

View File

@@ -456,10 +456,6 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
value: row.blockType,
}
if (addedByServer) {
state[fieldKey].addedByServer = addedByServer
}
if (includeSchema) {
state[fieldKey].fieldSchema = block.fields.find(
(blockField) => 'name' in blockField && blockField.name === 'blockType',

View File

@@ -1,41 +0,0 @@
import type { CollectionConfig } from 'payload'
import { placeholderCollectionSlug } from '../slugs.js'
export const Placeholder: CollectionConfig = {
slug: placeholderCollectionSlug,
fields: [
{
name: 'defaultSelect',
type: 'select',
options: [
{
label: 'Option 1',
value: 'option1',
},
],
},
{
name: 'placeholderSelect',
type: 'select',
options: [{ label: 'Option 1', value: 'option1' }],
admin: {
placeholder: 'Custom placeholder',
},
},
{
name: 'defaultRelationship',
type: 'relationship',
relationTo: 'posts',
},
{
name: 'placeholderRelationship',
type: 'relationship',
relationTo: 'posts',
admin: {
placeholder: 'Custom placeholder',
},
},
],
versions: true,
}

View File

@@ -1,6 +1,7 @@
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { Array } from './collections/Array.js'
import { BaseListFilter } from './collections/BaseListFilter.js'
@@ -18,7 +19,6 @@ import { CollectionHidden } from './collections/Hidden.js'
import { ListDrawer } from './collections/ListDrawer.js'
import { CollectionNoApiView } from './collections/NoApiView.js'
import { CollectionNotInView } from './collections/NotInView.js'
import { Placeholder } from './collections/Placeholder.js'
import { Posts } from './collections/Posts.js'
import { UploadCollection } from './collections/Upload.js'
import { UploadTwoCollection } from './collections/UploadTwo.js'
@@ -43,8 +43,7 @@ import {
protectedCustomNestedViewPath,
publicCustomViewPath,
} from './shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfigWithDefaults({
admin: {
importMap: {
@@ -166,7 +165,6 @@ export default buildConfigWithDefaults({
BaseListFilter,
with300Documents,
ListDrawer,
Placeholder,
],
globals: [
GlobalHidden,

View File

@@ -360,65 +360,6 @@ describe('Document View', () => {
await expect.poll(() => drawer2Left > drawerLeft).toBe(true)
})
test('document drawer displays a link to document', async () => {
await navigateToDoc(page, postsUrl)
// change the relationship to a document which is a different one than the current one
await page.locator('#field-relationship').click()
await page.locator('#field-relationship .rs__option').nth(2).click()
await saveDocAndAssert(page)
// open relationship drawer
await page
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
.click()
const drawer1Content = page.locator('[id^=doc-drawer_posts_1_] .drawer__content')
await expect(drawer1Content).toBeVisible()
// modify the title to trigger the leave page modal
await page.locator('.drawer__content #field-title').fill('New Title')
// Open link in a new tab by holding down the Meta or Control key
const documentLink = page.locator('.id-label a')
const documentId = String(await documentLink.textContent())
await documentLink.click()
const leavePageModal = page.locator('#leave-without-saving #confirm-action').last()
await expect(leavePageModal).toBeVisible()
await leavePageModal.click()
await page.waitForURL(postsUrl.edit(documentId))
})
test('document can be opened in a new tab from within the drawer', async () => {
await navigateToDoc(page, postsUrl)
await page
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
.click()
await wait(500)
const drawer1Content = page.locator('[id^=doc-drawer_posts_1_] .drawer__content')
await expect(drawer1Content).toBeVisible()
const currentUrl = page.url()
// Open link in a new tab by holding down the Meta or Control key
const documentLink = page.locator('.id-label a')
const documentId = String(await documentLink.textContent())
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
documentLink.click({ modifiers: ['ControlOrMeta'] }),
])
// Wait for navigation to complete in the new tab and ensure correct URL
await expect(newPage.locator('.doc-header')).toBeVisible()
// using contain here, because after load the lists view will add query params like "?limit=10"
expect(newPage.url()).toContain(postsUrl.edit(documentId))
// Ensure the original page did not change
expect(page.url()).toBe(currentUrl)
})
})
describe('descriptions', () => {

View File

@@ -1,10 +1,11 @@
import type { Page } from '@playwright/test'
import type { User as PayloadUser } from 'payload'
import { expect, test } from '@playwright/test'
import { mapAsync } from 'payload'
import * as qs from 'qs-esm'
import type { Config, Geo, Post } from '../../payload-types.js'
import type { Config, Geo, Post, User } from '../../payload-types.js'
import {
ensureCompilationIsDone,
@@ -20,7 +21,6 @@ import {
customViews1CollectionSlug,
geoCollectionSlug,
listDrawerSlug,
placeholderCollectionSlug,
postsCollectionSlug,
with300DocumentsSlug,
} from '../../slugs.js'
@@ -64,7 +64,6 @@ describe('List View', () => {
let customViewsUrl: AdminUrlUtil
let with300DocumentsUrl: AdminUrlUtil
let withListViewUrl: AdminUrlUtil
let placeholderUrl: AdminUrlUtil
let user: any
let serverURL: string
@@ -88,7 +87,7 @@ describe('List View', () => {
baseListFiltersUrl = new AdminUrlUtil(serverURL, 'base-list-filters')
customViewsUrl = new AdminUrlUtil(serverURL, customViews1CollectionSlug)
withListViewUrl = new AdminUrlUtil(serverURL, listDrawerSlug)
placeholderUrl = new AdminUrlUtil(serverURL, placeholderCollectionSlug)
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
@@ -393,24 +392,6 @@ describe('List View', () => {
await expect(page.locator(tableRowLocator)).toHaveCount(2)
})
test('should search for nested fields in field dropdown', async () => {
await page.goto(postsUrl.list)
await openListFilters(page, {})
const whereBuilder = page.locator('.where-builder')
await whereBuilder.locator('.where-builder__add-first-filter').click()
const conditionField = whereBuilder.locator('.condition__field')
await conditionField.click()
await conditionField.locator('input.rs__input').fill('Tab 1 > Title')
await expect(
conditionField.locator('.rs__menu-list').locator('div', {
hasText: exactText('Tab 1 > Title'),
}),
).toBeVisible()
})
test('should allow to filter in array field', async () => {
await createArray()
@@ -1427,66 +1408,6 @@ describe('List View', () => {
).toHaveText('Title')
})
})
describe('placeholder', () => {
test('should display placeholder in filter options', async () => {
await page.goto(
`${placeholderUrl.list}${qs.stringify(
{
where: {
or: [
{
and: [
{
defaultSelect: {
equals: '',
},
},
{
placeholderSelect: {
equals: '',
},
},
{
defaultRelationship: {
equals: '',
},
},
{
placeholderRelationship: {
equals: '',
},
},
],
},
],
},
},
{ addQueryPrefix: true },
)}`,
)
const conditionValueSelects = page.locator('#list-controls-where .condition__value')
await expect(conditionValueSelects.nth(0)).toHaveText('Select a value')
await expect(conditionValueSelects.nth(1)).toHaveText('Custom placeholder')
await expect(conditionValueSelects.nth(2)).toHaveText('Select a value')
await expect(conditionValueSelects.nth(3)).toHaveText('Custom placeholder')
})
})
test('should display placeholder in edit view', async () => {
await page.goto(placeholderUrl.create)
await expect(page.locator('#field-defaultSelect .rs__placeholder')).toHaveText('Select a value')
await expect(page.locator('#field-placeholderSelect .rs__placeholder')).toHaveText(
'Custom placeholder',
)
await expect(page.locator('#field-defaultRelationship .rs__placeholder')).toHaveText(
'Select a value',
)
await expect(page.locator('#field-placeholderRelationship .rs__placeholder')).toHaveText(
'Custom placeholder',
)
})
})
async function createPost(overrides?: Partial<Post>): Promise<Post> {

View File

@@ -14,7 +14,6 @@ export const noApiViewCollectionSlug = 'collection-no-api-view'
export const disableDuplicateSlug = 'disable-duplicate'
export const disableCopyToLocale = 'disable-copy-to-locale'
export const uploadCollectionSlug = 'uploads'
export const placeholderCollectionSlug = 'placeholder'
export const uploadTwoCollectionSlug = 'uploads-two'
export const customFieldsSlug = 'custom-fields'

View File

@@ -358,46 +358,6 @@ describe('relationship', () => {
).toHaveText(`${value}123456`)
})
test('should open related document in a new tab when meta key is applied', async () => {
await page.goto(url.create)
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
await openDocDrawer({
page,
selector:
'#field-relationWithAllowCreateToFalse .relationship--single-value__drawer-toggler',
withMetaKey: true,
}),
])
// Wait for navigation to complete in the new tab and ensure the edit view is open
await expect(newPage.locator('.collection-edit')).toBeVisible()
})
test('multi value relationship should open document in a new tab', async () => {
await page.goto(url.create)
// Select "Seeded text document" relationship
await page.locator('#field-relationshipHasMany .rs__control').click()
await page.locator('.rs__option:has-text("Seeded text document")').click()
await expect(
page.locator('#field-relationshipHasMany .relationship--multi-value-label__drawer-toggler'),
).toBeVisible()
const [newPage] = await Promise.all([
page.context().waitForEvent('page'),
await openDocDrawer({
page,
selector: '#field-relationshipHasMany .relationship--multi-value-label__drawer-toggler',
withMetaKey: true,
}),
])
// Wait for navigation to complete in the new tab and ensure the edit view is open
await expect(newPage.locator('.collection-edit')).toBeVisible()
})
// Drawers opened through the edit button are prone to issues due to the use of stopPropagation for certain
// events - specifically for drawers opened through the edit button. This test is to ensure that drawers
// opened through the edit button can be saved using the hotkey.

View File

@@ -228,12 +228,6 @@ describe('Form State', () => {
collection: postsSlug,
data: {
title: 'Test Post',
blocks: [
{
blockType: 'text',
text: 'Test block',
},
],
},
})
@@ -254,7 +248,6 @@ describe('Form State', () => {
})
expect(state.title?.addedByServer).toBe(true)
expect(state['blocks.0.blockType']?.addedByServer).toBe(true)
// Ensure that `addedByServer` is removed after being received by the client
const newState = mergeServerFormState({

View File

@@ -104,50 +104,5 @@ describe('graphql', () => {
expect(res.hyphenated_name).toStrictEqual('example-hyphenated-name')
})
it('should not error because of non nullable fields', async () => {
await payload.delete({ collection: 'posts', where: {} })
// this is an array if any errors
const res_1 = await restClient
.GRAPHQL_POST({
body: JSON.stringify({
query: `
query {
Posts {
docs {
title
}
prevPage
}
}
`,
}),
})
.then((res) => res.json())
expect(res_1.errors).toBeFalsy()
await payload.create({
collection: 'posts',
data: { title: 'any-title' },
})
const res_2 = await restClient
.GRAPHQL_POST({
body: JSON.stringify({
query: `
query {
Posts(limit: 1) {
docs {
title
}
}
}
`,
}),
})
.then((res) => res.json())
expect(res_2.errors).toBeFalsy()
})
})
})

View File

@@ -6,18 +6,12 @@ import { wait } from 'payload/shared'
export async function openDocDrawer({
page,
selector,
withMetaKey = false,
}: {
page: Page
selector: string
withMetaKey?: boolean
}): Promise<void> {
let clickProperties = {}
if (withMetaKey) {
clickProperties = { modifiers: ['ControlOrMeta'] }
}
await wait(500) // wait for parent form state to initialize
await page.locator(selector).click(clickProperties)
await page.locator(selector).click()
await wait(500) // wait for drawer form state to initialize
}

View File

@@ -1,9 +0,0 @@
import type { CollectionConfig } from 'payload'
import { mediaWithSignedDownloadsSlug } from '../shared.js'
export const MediaWithSignedDownloads: CollectionConfig = {
slug: mediaWithSignedDownloadsSlug,
upload: true,
fields: [],
}

View File

@@ -7,9 +7,8 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import { Media } from './collections/Media.js'
import { MediaWithPrefix } from './collections/MediaWithPrefix.js'
import { MediaWithSignedDownloads } from './collections/MediaWithSignedDownloads.js'
import { Users } from './collections/Users.js'
import { mediaSlug, mediaWithPrefixSlug, mediaWithSignedDownloadsSlug, prefix } from './shared.js'
import { mediaSlug, mediaWithPrefixSlug, prefix } from './shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -26,7 +25,7 @@ export default buildConfigWithDefaults({
baseDir: path.resolve(dirname),
},
},
collections: [Media, MediaWithPrefix, MediaWithSignedDownloads, Users],
collections: [Media, MediaWithPrefix, Users],
onInit: async (payload) => {
await payload.create({
collection: 'users',
@@ -43,9 +42,6 @@ export default buildConfigWithDefaults({
[mediaWithPrefixSlug]: {
prefix,
},
[mediaWithSignedDownloadsSlug]: {
signedDownloads: true,
},
},
bucket: process.env.S3_BUCKET,
config: {

View File

@@ -4,16 +4,12 @@ import * as AWS from '@aws-sdk/client-s3'
import path from 'path'
import { fileURLToPath } from 'url'
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
import { mediaSlug, mediaWithPrefixSlug, mediaWithSignedDownloadsSlug, prefix } from './shared.js'
import { mediaSlug, mediaWithPrefixSlug, prefix } from './shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
let restClient: NextRESTClient
let payload: Payload
describe('@payloadcms/storage-s3', () => {
@@ -21,7 +17,7 @@ describe('@payloadcms/storage-s3', () => {
let client: AWS.S3Client
beforeAll(async () => {
;({ payload, restClient } = await initPayloadInt(dirname))
;({ payload } = await initPayloadInt(dirname))
TEST_BUCKET = process.env.S3_BUCKET
client = new AWS.S3({
@@ -81,38 +77,15 @@ describe('@payloadcms/storage-s3', () => {
expect(upload.url).toEqual(`/api/${mediaWithPrefixSlug}/file/${String(upload.filename)}`)
})
it('can download with signed downloads', async () => {
await payload.create({
collection: mediaWithSignedDownloadsSlug,
data: {},
filePath: path.resolve(dirname, '../uploads/image.png'),
})
const response = await restClient.GET(`/${mediaWithSignedDownloadsSlug}/file/image.png`)
expect(response.status).toBe(302)
const url = response.headers.get('Location')
expect(url).toBeDefined()
expect(url!).toContain(`/${TEST_BUCKET}/image.png`)
expect(new URLSearchParams(url!).get('x-id')).toBe('GetObject')
const file = await fetch(url!)
expect(file.headers.get('Content-Type')).toBe('image/png')
})
describe('R2', () => {
it.todo('can upload')
})
async function createTestBucket() {
try {
const makeBucketRes = await client.send(new AWS.CreateBucketCommand({ Bucket: TEST_BUCKET }))
const makeBucketRes = await client.send(new AWS.CreateBucketCommand({ Bucket: TEST_BUCKET }))
if (makeBucketRes.$metadata.httpStatusCode !== 200) {
throw new Error(`Failed to create bucket. ${makeBucketRes.$metadata.httpStatusCode}`)
}
} catch (e) {
if (e instanceof AWS.BucketAlreadyOwnedByYou) {
console.log('Bucket already exists')
}
if (makeBucketRes.$metadata.httpStatusCode !== 200) {
throw new Error(`Failed to create bucket. ${makeBucketRes.$metadata.httpStatusCode}`)
}
}
@@ -123,9 +96,7 @@ describe('@payloadcms/storage-s3', () => {
}),
)
if (!listedObjects?.Contents?.length) {
return
}
if (!listedObjects?.Contents?.length) return
const deleteParams = {
Bucket: TEST_BUCKET,

View File

@@ -69,7 +69,6 @@ export interface Config {
collections: {
media: Media;
'media-with-prefix': MediaWithPrefix;
'media-with-signed-downloads': MediaWithSignedDownload;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
@@ -79,7 +78,6 @@ export interface Config {
collectionsSelect: {
media: MediaSelect<false> | MediaSelect<true>;
'media-with-prefix': MediaWithPrefixSelect<false> | MediaWithPrefixSelect<true>;
'media-with-signed-downloads': MediaWithSignedDownloadsSelect<false> | MediaWithSignedDownloadsSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -173,24 +171,6 @@ export interface MediaWithPrefix {
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media-with-signed-downloads".
*/
export interface MediaWithSignedDownload {
id: string;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
@@ -223,10 +203,6 @@ export interface PayloadLockedDocument {
relationTo: 'media-with-prefix';
value: string | MediaWithPrefix;
} | null)
| ({
relationTo: 'media-with-signed-downloads';
value: string | MediaWithSignedDownload;
} | null)
| ({
relationTo: 'users';
value: string | User;
@@ -333,23 +309,6 @@ export interface MediaWithPrefixSelect<T extends boolean = true> {
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media-with-signed-downloads_select".
*/
export interface MediaWithSignedDownloadsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".

View File

@@ -1,5 +1,3 @@
export const mediaSlug = 'media'
export const mediaWithPrefixSlug = 'media-with-prefix'
export const prefix = 'test-prefix'
export const mediaWithSignedDownloadsSlug = 'media-with-signed-downloads'

View File

@@ -1,33 +0,0 @@
import type { CollectionConfig } from 'payload'
import { APIError } from 'payload'
import { errorOnUnpublishSlug } from '../slugs.js'
const ErrorOnUnpublish: CollectionConfig = {
slug: errorOnUnpublishSlug,
admin: {
useAsTitle: 'title',
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
],
versions: {
drafts: true,
},
hooks: {
beforeValidate: [
({ data, originalDoc }) => {
if (data?._status === 'draft' && originalDoc?._status === 'published') {
throw new APIError('Custom error on unpublish', 400, {}, true)
}
},
],
},
}
export default ErrorOnUnpublish

View File

@@ -12,7 +12,6 @@ import DisablePublish from './collections/DisablePublish.js'
import DraftPosts from './collections/Drafts.js'
import DraftWithMax from './collections/DraftsWithMax.js'
import DraftsWithValidate from './collections/DraftsWithValidate.js'
import ErrorOnUnpublish from './collections/ErrorOnUnpublish.js'
import LocalizedPosts from './collections/Localized.js'
import { Media } from './collections/Media.js'
import Posts from './collections/Posts.js'
@@ -43,7 +42,6 @@ export default buildConfigWithDefaults({
DraftPosts,
DraftWithMax,
DraftsWithValidate,
ErrorOnUnpublish,
LocalizedPosts,
VersionPosts,
CustomIDs,

View File

@@ -60,7 +60,6 @@ import {
draftWithMaxCollectionSlug,
draftWithMaxGlobalSlug,
draftWithValidateCollectionSlug,
errorOnUnpublishSlug,
localizedCollectionSlug,
localizedGlobalSlug,
postCollectionSlug,
@@ -87,7 +86,6 @@ describe('Versions', () => {
let disablePublishURL: AdminUrlUtil
let customIDURL: AdminUrlUtil
let postURL: AdminUrlUtil
let errorOnUnpublishURL: AdminUrlUtil
let id: string
beforeAll(async ({ browser }, testInfo) => {
@@ -126,7 +124,6 @@ describe('Versions', () => {
disablePublishURL = new AdminUrlUtil(serverURL, disablePublishSlug)
customIDURL = new AdminUrlUtil(serverURL, customIDSlug)
postURL = new AdminUrlUtil(serverURL, postCollectionSlug)
errorOnUnpublishURL = new AdminUrlUtil(serverURL, errorOnUnpublishSlug)
})
test('collection — has versions tab', async () => {
@@ -208,116 +205,6 @@ describe('Versions', () => {
await expect(page.locator('#field-title')).toHaveValue('v1')
})
test('should show currently published version status in versions view', async () => {
const publishedDoc = await payload.create({
collection: draftCollectionSlug,
data: {
_status: 'published',
title: 'title',
description: 'description',
},
overrideAccess: true,
})
await page.goto(`${url.edit(publishedDoc.id)}/versions`)
await expect(page.locator('main.versions')).toContainText('Current Published Version')
})
test('should show unpublished version status in versions view', async () => {
const publishedDoc = await payload.create({
collection: draftCollectionSlug,
data: {
_status: 'published',
title: 'title',
description: 'description',
},
overrideAccess: true,
})
// Unpublish the document
await payload.update({
collection: draftCollectionSlug,
id: publishedDoc.id,
data: {
_status: 'draft',
},
draft: false,
})
await page.goto(`${url.edit(publishedDoc.id)}/versions`)
await expect(page.locator('main.versions')).toContainText('Previously Published')
})
test('should show global versions view level action in globals versions view', async () => {
const global = new AdminUrlUtil(serverURL, draftGlobalSlug)
await page.goto(`${global.global(draftGlobalSlug)}/versions`)
await expect(page.locator('.app-header .global-versions-button')).toHaveCount(1)
})
test('global — has versions tab', async () => {
const global = new AdminUrlUtil(serverURL, draftGlobalSlug)
await page.goto(global.global(draftGlobalSlug))
const docURL = page.url()
const pathname = new URL(docURL).pathname
const versionsTab = page.locator('.doc-tab', {
hasText: 'Versions',
})
await versionsTab.waitFor({ state: 'visible' })
expect(versionsTab).toBeTruthy()
const href = versionsTab.locator('a').first()
await expect(href).toHaveAttribute('href', `${pathname}/versions`)
})
test('global — respects max number of versions', async () => {
await payload.updateGlobal({
slug: draftWithMaxGlobalSlug,
data: {
title: 'initial title',
},
})
const global = new AdminUrlUtil(serverURL, draftWithMaxGlobalSlug)
await page.goto(global.global(draftWithMaxGlobalSlug))
const titleFieldInitial = page.locator('#field-title')
await titleFieldInitial.fill('updated title')
await saveDocAndAssert(page, '#action-save-draft')
await expect(titleFieldInitial).toHaveValue('updated title')
const versionsTab = page.locator('.doc-tab', {
hasText: '1',
})
await versionsTab.waitFor({ state: 'visible' })
expect(versionsTab).toBeTruthy()
const titleFieldUpdated = page.locator('#field-title')
await titleFieldUpdated.fill('latest title')
await saveDocAndAssert(page, '#action-save-draft')
await expect(titleFieldUpdated).toHaveValue('latest title')
const versionsTabUpdated = page.locator('.doc-tab', {
hasText: '1',
})
await versionsTabUpdated.waitFor({ state: 'visible' })
expect(versionsTabUpdated).toBeTruthy()
})
test('global — has versions route', async () => {
const global = new AdminUrlUtil(serverURL, autoSaveGlobalSlug)
const versionsURL = `${global.global(autoSaveGlobalSlug)}/versions`
await page.goto(versionsURL)
await expect(() => {
expect(page.url()).toMatch(/\/versions/)
}).toPass({ timeout: 10000, intervals: [100] })
})
test('collection - should autosave', async () => {
await page.goto(autosaveURL.create)
await page.locator('#field-title').fill('autosave title')
@@ -582,22 +469,6 @@ describe('Versions', () => {
await expect(page.locator('#action-save')).not.toBeAttached()
})
test('collections — should show custom error message when unpublishing fails', async () => {
const publishedDoc = await payload.create({
collection: errorOnUnpublishSlug,
data: {
_status: 'published',
title: 'title',
},
})
await page.goto(errorOnUnpublishURL.edit(String(publishedDoc.id)))
await page.locator('#action-unpublish').click()
await page.locator('[id^="confirm-un-publish-"] #confirm-action').click()
await expect(
page.locator('.payload-toast-item:has-text("Custom error on unpublish")'),
).toBeVisible()
})
test('should show documents title in relationship even if draft document', async () => {
await payload.create({
collection: autosaveCollectionSlug,

View File

@@ -75,7 +75,6 @@ export interface Config {
'draft-posts': DraftPost;
'draft-with-max-posts': DraftWithMaxPost;
'draft-with-validate-posts': DraftWithValidatePost;
'error-on-unpublish': ErrorOnUnpublish;
'localized-posts': LocalizedPost;
'version-posts': VersionPost;
'custom-ids': CustomId;
@@ -98,7 +97,6 @@ export interface Config {
'draft-posts': DraftPostsSelect<false> | DraftPostsSelect<true>;
'draft-with-max-posts': DraftWithMaxPostsSelect<false> | DraftWithMaxPostsSelect<true>;
'draft-with-validate-posts': DraftWithValidatePostsSelect<false> | DraftWithValidatePostsSelect<true>;
'error-on-unpublish': ErrorOnUnpublishSelect<false> | ErrorOnUnpublishSelect<true>;
'localized-posts': LocalizedPostsSelect<false> | LocalizedPostsSelect<true>;
'version-posts': VersionPostsSelect<false> | VersionPostsSelect<true>;
'custom-ids': CustomIdsSelect<false> | CustomIdsSelect<true>;
@@ -291,17 +289,6 @@ export interface DraftWithValidatePost {
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "error-on-unpublish".
*/
export interface ErrorOnUnpublish {
id: string;
title: string;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localized-posts".
@@ -602,10 +589,6 @@ export interface PayloadLockedDocument {
relationTo: 'draft-with-validate-posts';
value: string | DraftWithValidatePost;
} | null)
| ({
relationTo: 'error-on-unpublish';
value: string | ErrorOnUnpublish;
} | null)
| ({
relationTo: 'localized-posts';
value: string | LocalizedPost;
@@ -795,16 +778,6 @@ export interface DraftWithValidatePostsSelect<T extends boolean = true> {
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "error-on-unpublish_select".
*/
export interface ErrorOnUnpublishSelect<T extends boolean = true> {
title?: T;
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localized-posts_select".

Some files were not shown because too many files have changed in this diff Show More