Compare commits

..

30 Commits

Author SHA1 Message Date
Elliot DeNolf
c4ee623907 chore(release): v3.0.0-beta.75 [skip ci] 2024-08-07 14:01:09 -04:00
James Mikrut
1cb1e5e8b3 feat(db-*): allows for running migrations in production automatically (#7563)
## Description

Introduces a pattern for running migrations upon Payload init in
production.
2024-08-07 13:57:12 -04:00
Paul
e0699838e1 chore(ui): update useField jsdoc comment to point to the right internal hook (#7568) 2024-08-07 17:49:32 +00:00
Dan Ribbens
46f70d9df4 fix(db-postgres): #7492 migrate snapshots (#7540)
## Description

Fixes #7492 

In order to run createMigration, we need to read in the previous
snapshot file if one exists. When that snapshot was generated from an
older version of drizzle-kit, we have to first migrate it up match the
latest version for drizzle to generate the new migration. This change
adds in the call to check the version and migrate the snapshot if
needed.
2024-08-07 13:49:08 -04:00
Paul
b7e2c59622 fix(plugin-seo): issue with generating image from a function (#7566) 2024-08-07 17:35:43 +00:00
Jarrod Flesch
0cc7184023 fix: hydrate permissions on dashboard, fix active menu item logic 2024-08-07 12:14:58 -04:00
Jarrod Flesch
e905675a05 chore!: adjusts auth hydration from server (#7545)
Fixes https://github.com/payloadcms/payload/issues/6823

Allows the server to initialize the AuthProvider via props. Renames
`HydrateClientUser` to `HydrateAuthProvider`. It now only hydrates the
permissions as the user can be set from props. Permissions can be
initialized from props, but still need to be hydrated for some pages as
access control can be specific to docs/lists etc.

**BREAKING CHANGE**
- Renames exported `HydrateClientUser` to `HydrateAuthProvider`
2024-08-07 11:10:53 -04:00
Paul
4a20a63563 fix(ui): fixes issue when filtering by checkbox value in a different language (#7547)
Fixes https://github.com/payloadcms/payload/issues/7447
2024-08-07 00:48:50 +00:00
Paul
8d1fc6e8fb feat!: bump next canary to 104 and update withPayload for new config (#7541)
We are now bumping up the Next canary version to `15.0.0-canary.104` and
`react` and `react-dom` to `^19.0.0-rc-06d0b89e-20240801`.

Your new dependencies should look like this:
```
"next": "15.0.0-canary.104",
"react": "^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0-rc-06d0b89e-20240801",
```

---------

Co-authored-by: Alessio Gravili <alessio@gravili.de>
2024-08-06 23:54:34 +00:00
Patrik
62744e79ac fix(next, payload): enable relationship & upload version tracking when localization enabled (#7508) 2024-08-06 12:28:06 -04:00
Jarrod Flesch
e8bed7b315 chore: call refresh after the subscription is ready, fixes CI (#7542)
LivePreview data was stale if the user entered data while the socket
connection was being established. This change ensures fresh data is
fetched after the connection is established.

This is easy to see when turning on 4G connection and in CI, where it is
especially slow.
2024-08-06 12:17:50 -04:00
Alessio Gravili
f2b8ddb299 Merge PR: fix lexical upload html converter, export missing nodes #7539
Fixes https://github.com/payloadcms/payload/issues/7495

When the Upload HTML Converter was called from the local API, the upload
document did not populate properly due to overrideAccess not being
passed through to the dataloader. This PR also adds new properties to
the afterRead field hook, so that these can be used in the lexical html
field.

Reproduction here:
https://github.com/payloadcms/payload/tree/chore/reproduce-html-converter-issue

**BREAKING:** If you define your own, custom lexical HTML Converters
that have sub-nodes, or if you directly call the
`convertLexicalNodesToHTML` function anywhere, you now need to pass
through the `showHiddenFields`, draft and `overrideAccess` props to the
`convertLexicalNodesToHTML` function. These are available in the
arguments of your HTML Converter function
2024-08-06 12:04:56 -04:00
Alessio Gravili
ffd8ea516d feat(richtext-lexical): export serialized inline blocks and table node types 2024-08-06 11:42:09 -04:00
Paul
3bf09703e9 chore: turn off autocomplete for create first user form (#7538)
Turns off autocomplete on the first user form so it doesn't conflict
with wrong credentials being autofilled
2024-08-06 15:34:26 +00:00
Alessio Gravili
c15d679b65 fix(richtext-lexical)!: html converters not respecting overrideAccess property when populating values, in local API 2024-08-06 11:15:47 -04:00
Jarrod Flesch
a422a0d568 fix: scopes preferences queries and mutations by user (#7534)
Fixes https://github.com/payloadcms/payload/issues/7530

Properly scopes preferences queries/mutations by user.
2024-08-06 10:35:46 -04:00
Jarrod Flesch
edaeb1e29f fix: ensure pw confirmation when creating users in admin panel (#7535) 2024-08-06 10:31:08 -04:00
Jessica Chowdhury
6f35c356fe fix: custom meta icons getting overwritten by default icon (#7466)
## Description

Issue reported by Trading Point.

Payload favicon is still shown even when a custom icon is provided.

To replicate add to Payload config:
```ts
  admin: {
    meta: {
      icons: [
        {
          url: '/images/test.jpg',
          fetchPriority: 'high',
          sizes: '16x16',
        },
      ],
    },
  },
```

- [X] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [X] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [ ] I have added tests that prove my fix is effective or that my
feature works
- [X] Existing test suite passes locally with my changes
- [ ] I have made corresponding changes to the documentation
2024-08-06 14:01:46 +00:00
Elliot DeNolf
0b9397399a chore(release): v3.0.0-beta.74 [skip ci] 2024-08-06 09:38:20 -04:00
Jarrod Flesch
cdcc35ccdb chore: fixes build error stemming from LoginField (#7532) 2024-08-06 09:15:21 -04:00
Jarrod Flesch
442189ec48 fix: email and username fields rendering in drawers (#7520)
Fixes https://github.com/payloadcms/payload/issues/7428

Now email and username fields are rendered with the RenderFields
component, making them behave similarly to other fields. They now appear
and can respect doc permissions, readOnly settings, etc.
2024-08-05 20:18:32 -04:00
Alessio Gravili
5d1cc760c9 fix(richtext-lexical): various table and icon style issues (#7522) 2024-08-05 22:10:18 +00:00
Alessio Gravili
2f90683c7d Merge PR: upgrade lexical, add table feature converter (#7521)
This PR
- upgrades lexical and ports all bug fixes from the playground over
- adds table action buttons. When hovering the edges of the table,
buttons pop up to easily add a new table column or row
- adds an html converter for the table feature
- makes the placeholder shown in the editor when no text is present
accessible

**BREAKING:** This upgrades lexical from 0.16.1 to 0.17.0. If you have
any lexical packages installed in your project, please update them
accordingly. Additionally, if you depend on the lexical APIs, please
consult their changelog, as lexical may introduce breaking changes:
https://github.com/facebook/lexical/releases/tag/v0.17.0
2024-08-05 17:18:57 -04:00
Patrik
3f5403a52a fix(ui): prevents hasMany text going outside of input boundaries (#7455)
## Description

V2 PR [here](https://github.com/payloadcms/payload/pull/7454)

`Before`:
![Screenshot 2024-07-31 at 12 40
50 PM](https://github.com/user-attachments/assets/ce61f4fc-e676-4273-aa4c-72610cb459b3)

`After`:
![Screenshot 2024-07-31 at 12 40
23 PM](https://github.com/user-attachments/assets/d92631eb-28fb-46ca-bc23-46c7916bba34)

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-08-05 17:10:35 -04:00
Alessio Gravili
9bccdfd60a feat(richtext-lexical): add HTML converter to table feature 2024-08-05 17:01:21 -04:00
Patrik
62666a9897 fix(ui): properly handles ID field component type based on payload.db.defaultIDType (#7416)
## Description

Fixes #7354 

Since the `defaultIDType` for IDs in `postgres` are of type `number` -
the `contains` operator should be available in the filter options.

This PR checks the `defaultIDType` of ID and properly outputs the
correct component type for IDs

I.e if ID is of type `number` - the filter operators for ID should
correspond to the the operators of type number as well

The `contains` operator only belongs on fields of type string, aka of
component type `text`

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-08-05 16:39:27 -04:00
Alessio Gravili
eb27b84854 chore(richtext-lexical): backport various minor bugfixes from lexical playground 2024-08-05 16:35:13 -04:00
Alessio Gravili
c3480811d3 feat(richtext-lexical): accessible editor placeholders 2024-08-05 16:19:02 -04:00
Alessio Gravili
12ba820de4 feat(richtext-lexical): add table hover actions to quickly add rows or columns 2024-08-05 16:08:31 -04:00
Alessio Gravili
e65b6478c9 feat(richtext-lexical)!: upgrade lexical from 0.16.1 to 0.17.0 2024-08-05 09:58:27 -04:00
166 changed files with 2155 additions and 2298 deletions

14
.vscode/launch.json vendored
View File

@@ -56,6 +56,20 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js fields-relationship",
"cwd": "${workspaceFolder}",
"name": "Run Dev Fields-Relationship",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js login-with-username",
"cwd": "${workspaceFolder}",
"name": "Run Dev Login-With-Username",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev plugin-cloud-storage",
"cwd": "${workspaceFolder}",

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"private": true,
"type": "module",
"scripts": {
@@ -96,7 +96,7 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@libsql/client": "0.6.2",
"@next/bundle-analyzer": "15.0.0-canary.53",
"@next/bundle-analyzer": "15.0.0-canary.104",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
"@payloadcms/live-preview-react": "workspace:*",
@@ -131,15 +131,15 @@
"lint-staged": "15.2.7",
"minimist": "1.2.8",
"mongodb-memory-server": "^9.0",
"next": "15.0.0-canary.53",
"next": "15.0.0-canary.104",
"open": "^10.1.0",
"p-limit": "^5.0.0",
"playwright": "1.43.0",
"playwright-core": "1.43.0",
"prettier": "3.3.2",
"prompts": "2.4.2",
"react": "^19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0-rc-6230622a1a-20240610",
"react": "^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0-rc-06d0b89e-20240801",
"rimraf": "3.0.2",
"semver": "^7.5.4",
"sharp": "0.32.6",
@@ -153,8 +153,8 @@
"typescript": "5.5.4"
},
"peerDependencies": {
"react": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610"
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801"
},
"engines": {
"node": "^18.20.2 || >=20.9.0",

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

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

View File

@@ -54,6 +54,10 @@ export const connect: Connect = async function connect(
this.payload.logger.info('---- DROPPED DATABASE ----')
}
}
if (process.env.NODE_ENV === 'production' && this.prodMigrations) {
await this.migrate({ migrations: this.prodMigrations })
}
} catch (err) {
console.log(err)
this.payload.logger.error(`Error: cannot connect to MongoDB. Details: ${err.message}`, err)

View File

@@ -8,7 +8,7 @@ import mongoose from 'mongoose'
import path from 'path'
import { createDatabaseAdapter } from 'payload'
import type { CollectionModel, GlobalModel } from './types.js'
import type { CollectionModel, GlobalModel, MigrateDownArgs, MigrateUpArgs } from './types.js'
import { connect } from './connect.js'
import { count } from './count.js'
@@ -78,6 +78,11 @@ export interface Args {
* typed as any to avoid dependency
*/
mongoMemoryServer?: MongoMemoryReplSet
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
transactionOptions?: TransactionOptions | false
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
url: false | string
@@ -90,6 +95,11 @@ export type MongooseAdapter = {
connection: Connection
globals: GlobalModel
mongoMemoryServer: MongoMemoryReplSet
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
sessions: Record<number | string, ClientSession>
versions: {
[slug: string]: CollectionModel
@@ -107,6 +117,11 @@ declare module 'payload' {
connection: Connection
globals: GlobalModel
mongoMemoryServer: MongoMemoryReplSet
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
sessions: Record<number | string, ClientSession>
transactionOptions: TransactionOptions
versions: {
@@ -121,6 +136,7 @@ export function mongooseAdapter({
disableIndexHints = false,
migrationDir: migrationDirArg,
mongoMemoryServer,
prodMigrations,
transactionOptions = {},
url,
}: Args): DatabaseAdapterObj {
@@ -167,6 +183,7 @@ export function mongooseAdapter({
migrateFresh,
migrationDir,
payload,
prodMigrations,
queryDrafts,
rollbackTransaction,
updateGlobal,

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -47,7 +47,7 @@
"dependencies": {
"@payloadcms/drizzle": "workspace:*",
"console-table-printer": "2.11.2",
"drizzle-kit": "0.23.2",
"drizzle-kit": "0.23.2-df9e596",
"drizzle-orm": "0.32.1",
"pg": "8.11.3",
"prompts": "2.4.2",

View File

@@ -91,4 +91,8 @@ export const connect: Connect = async function connect(
}
if (typeof this.resolveInitializing === 'function') this.resolveInitializing()
if (process.env.NODE_ENV === 'production' && this.prodMigrations) {
await this.migrate({ migrations: this.prodMigrations })
}
}

View File

@@ -4,7 +4,7 @@ import type { CreateMigration } from 'payload'
import fs from 'fs'
import { createRequire } from 'module'
import path from 'path'
import { getPredefinedMigration } from 'payload'
import { getPredefinedMigration, writeMigrationIndex } from 'payload'
import prompts from 'prompts'
import { fileURLToPath } from 'url'
@@ -25,7 +25,7 @@ export const createMigration: CreateMigration = async function createMigration(
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/api')
const { generateDrizzleJson, generateMigration, upPgSnapshot } = require('drizzle-kit/api')
const drizzleJsonAfter = generateDrizzleJson(this.schema)
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
const formattedDate = yyymmdd.replace(/\D/g, '')
@@ -64,9 +64,11 @@ export const createMigration: CreateMigration = async function createMigration(
.reverse()?.[0]
if (latestSnapshot) {
drizzleJsonBefore = JSON.parse(
fs.readFileSync(`${dir}/${latestSnapshot}`, 'utf8'),
) as DrizzleSnapshotJSON
drizzleJsonBefore = JSON.parse(fs.readFileSync(`${dir}/${latestSnapshot}`, 'utf8'))
if (drizzleJsonBefore.version < drizzleJsonAfter.version) {
drizzleJsonBefore = upPgSnapshot(drizzleJsonBefore)
}
}
const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
@@ -113,5 +115,8 @@ export const createMigration: CreateMigration = async function createMigration(
upSQL: upSQL || ` // Migration code`,
}),
)
writeMigrationIndex({ migrationsDir: payload.db.migrationDir })
payload.logger.info({ msg: `Migration created at ${filePath}.ts` })
}

View File

@@ -32,7 +32,7 @@ import {
updateOne,
updateVersion,
} from '@payloadcms/drizzle'
import { type PgSchema, pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { createDatabaseAdapter } from 'payload'
import type { Args, PostgresAdapter } from './types.js'
@@ -94,6 +94,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
pgSchema: adapterSchema,
pool: undefined,
poolOptions: args.pool,
prodMigrations: args.prodMigrations,
push: args.push,
relations: {},
relationshipsSuffix: args.relationshipsSuffix || '_rels',

View File

@@ -33,6 +33,11 @@ export type Args = {
logger?: DrizzleConfig['logger']
migrationDir?: string
pool: PoolConfig
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
push?: boolean
relationshipsSuffix?: string
/**
@@ -136,6 +141,11 @@ export type PostgresAdapter = {
pgSchema?: Schema
pool: Pool
poolOptions: Args['pool']
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
push: boolean
rejectInitializing: () => void
relations: Record<string, GenericRelation>
@@ -178,6 +188,11 @@ declare module 'payload' {
pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool
poolOptions: Args['pool']
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
push: boolean
rejectInitializing: () => void
relationshipsSuffix?: string

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -46,7 +46,7 @@
"@libsql/client": "^0.6.2",
"@payloadcms/drizzle": "workspace:*",
"console-table-printer": "2.11.2",
"drizzle-kit": "0.23.2",
"drizzle-kit": "0.23.2-df9e596",
"drizzle-orm": "0.32.1",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",

View File

@@ -52,4 +52,8 @@ export const connect: Connect = async function connect(
}
if (typeof this.resolveInitializing === 'function') this.resolveInitializing()
if (process.env.NODE_ENV === 'production' && this.prodMigrations) {
await this.migrate({ migrations: this.prodMigrations })
}
}

View File

@@ -4,7 +4,7 @@ import type { CreateMigration } from 'payload'
import fs from 'fs'
import { createRequire } from 'module'
import path from 'path'
import { getPredefinedMigration } from 'payload'
import { getPredefinedMigration, writeMigrationIndex } from 'payload'
import prompts from 'prompts'
import { fileURLToPath } from 'url'
@@ -112,5 +112,8 @@ export const createMigration: CreateMigration = async function createMigration(
upSQL: upSQL || ` // Migration code`,
}),
)
writeMigrationIndex({ migrationsDir: payload.db.migrationDir })
payload.logger.info({ msg: `Migration created at ${filePath}.ts` })
}

View File

@@ -93,6 +93,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger,
operators,
prodMigrations: args.prodMigrations,
push: args.push,
relations: {},
relationshipsSuffix: args.relationshipsSuffix || '_rels',

View File

@@ -1,4 +1,4 @@
import type { AnySQLiteColumn} from 'drizzle-orm/sqlite-core';
import type { AnySQLiteColumn } from 'drizzle-orm/sqlite-core'
import { index, uniqueIndex } from 'drizzle-orm/sqlite-core'

View File

@@ -18,6 +18,11 @@ export type Args = {
localesSuffix?: string
logger?: DrizzleConfig['logger']
migrationDir?: string
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
push?: boolean
relationshipsSuffix?: string
schemaName?: string
@@ -100,6 +105,11 @@ export type SQLiteAdapter = {
localesSuffix?: string
logger: DrizzleConfig['logger']
operators: Operators
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
push: boolean
rejectInitializing: () => void
relations: Record<string, GenericRelation>
@@ -139,6 +149,11 @@ declare module 'payload' {
initializing: Promise<void>
localesSuffix?: string
logger: DrizzleConfig['logger']
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
push: boolean
rejectInitializing: () => void
relationshipsSuffix?: string

View File

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

View File

@@ -1,4 +1,3 @@
import type { Payload, PayloadRequest } from 'payload'
import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload'
@@ -9,9 +8,12 @@ import type { DrizzleAdapter, Migration } from './types.js'
import { migrationTableExists } from './utilities/migrationTableExists.js'
import { parseError } from './utilities/parseError.js'
export async function migrate(this: DrizzleAdapter): Promise<void> {
export const migrate: DrizzleAdapter['migrate'] = async function migrate(
this: DrizzleAdapter,
args,
): Promise<void> {
const { payload } = this
const migrationFiles = await readMigrationFiles({ payload })
const migrationFiles = args?.migrations || (await readMigrationFiles({ payload }))
if (!migrationFiles.length) {
payload.logger.info({ msg: 'No migrations to run.' })
@@ -64,7 +66,7 @@ export async function migrate(this: DrizzleAdapter): Promise<void> {
// If already ran, skip
if (alreadyRan) {
continue
continue
}
await runMigrationFile(payload, migration, newBatch)

View File

@@ -42,7 +42,7 @@ export async function migrateFresh(
await this.dropDatabase({ adapter: this })
const migrationFiles = (await readMigrationFiles({ payload })) as Migration[]
const migrationFiles = await readMigrationFiles({ payload })
payload.logger.debug({
msg: `Found ${migrationFiles.length} migration files.`,
})

View File

@@ -133,7 +133,7 @@ export type Migration = {
db?: DrizzleTransaction | LibSQLDatabase<Record<string, never>> | PostgresDB
payload: Payload
req: PayloadRequest
}) => Promise<boolean>
}) => Promise<void>
up: ({
db,
payload,
@@ -142,7 +142,7 @@ export type Migration = {
db?: DrizzleTransaction | LibSQLDatabase | PostgresDB
payload: Payload
req: PayloadRequest
}) => Promise<boolean>
}) => Promise<void>
} & MigrationData
export type CreateJSONQueryArgs = {

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {
@@ -41,8 +41,8 @@
"payload": "workspace:*"
},
"peerDependencies": {
"react": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610"
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801"
},
"publishConfig": {
"exports": {

View File

@@ -39,6 +39,9 @@ export const RefreshRouteOnSave: React.FC<{
ready({
serverURL,
})
// refresh after the ready message is sent to get the latest data
refresh()
}
return () => {
@@ -46,7 +49,7 @@ export const RefreshRouteOnSave: React.FC<{
window.removeEventListener('message', onMessage)
}
}
}, [serverURL, onMessage, depth, apiRoute])
}, [serverURL, onMessage, depth, apiRoute, refresh])
return null
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"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.0.0-beta.73",
"version": "3.0.0-beta.75",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -94,7 +94,7 @@
},
"peerDependencies": {
"graphql": "^16.8.1",
"next": "^15.0.0-canary.53",
"next": "^15.0.0-canary.104",
"payload": "workspace:*"
},
"engines": {

View File

@@ -1,45 +1,103 @@
'use client'
import type { LoginWithUsernameOptions } from 'payload'
import type { FieldPermissions, LoginWithUsernameOptions } from 'payload'
import { EmailField, TextField, useTranslation } from '@payloadcms/ui'
import { EmailField, RenderFields, TextField, useTranslation } from '@payloadcms/ui'
import { email, username } from 'payload/shared'
import React from 'react'
type Props = {
loginWithUsername?: LoginWithUsernameOptions | false
}
export const EmailAndUsernameFields: React.FC<Props> = ({ loginWithUsername }) => {
function EmailFieldComponent(props: Props) {
const { loginWithUsername } = props
const { t } = useTranslation()
const requireEmail = !loginWithUsername || (loginWithUsername && loginWithUsername.requireEmail)
const requireUsername = loginWithUsername && loginWithUsername.requireUsername
const showEmailField =
!loginWithUsername || loginWithUsername?.requireEmail || loginWithUsername?.allowEmailLogin
if (showEmailField) {
return (
<EmailField
autoComplete="off"
label={t('general:email')}
name="email"
path="email"
required={requireEmail}
validate={email}
/>
)
}
return null
}
function UsernameFieldComponent(props: Props) {
const { loginWithUsername } = props
const { t } = useTranslation()
const requireUsername = loginWithUsername && loginWithUsername.requireUsername
const showUsernameField = Boolean(loginWithUsername)
return (
<React.Fragment>
{showEmailField && (
<EmailField
autoComplete="email"
label={t('general:email')}
name="email"
path="email"
required={requireEmail}
validate={email}
/>
)}
if (showUsernameField) {
return (
<TextField
label={t('authentication:username')}
name="username"
path="username"
required={requireUsername}
validate={username}
/>
)
}
{showUsernameField && (
<TextField
label={t('authentication:username')}
name="username"
path="username"
required={requireUsername}
validate={username}
/>
)}
</React.Fragment>
return null
}
type RenderEmailAndUsernameFieldsProps = {
className?: string
loginWithUsername?: LoginWithUsernameOptions | false
operation?: 'create' | 'update'
permissions?: {
[fieldName: string]: FieldPermissions
}
readOnly: boolean
}
export function RenderEmailAndUsernameFields(props: RenderEmailAndUsernameFieldsProps) {
const { className, loginWithUsername, operation, permissions, readOnly } = props
return (
<RenderFields
className={className}
fieldMap={[
{
name: 'email',
type: 'text',
CustomField: <EmailFieldComponent loginWithUsername={loginWithUsername} />,
cellComponentProps: null,
fieldComponentProps: { type: 'email', autoComplete: 'off', readOnly },
fieldIsPresentational: false,
isFieldAffectingData: true,
localized: false,
},
{
name: 'username',
type: 'text',
CustomField: <UsernameFieldComponent loginWithUsername={loginWithUsername} />,
cellComponentProps: null,
fieldComponentProps: { type: 'text', readOnly },
fieldIsPresentational: false,
isFieldAffectingData: true,
localized: false,
},
]}
forceRender
operation={operation}
path=""
permissions={permissions}
readOnly={readOnly}
schemaPath=""
/>
)
}

View File

@@ -14,6 +14,7 @@ import {
} from '@payloadcms/ui'
import { EntityType, formatAdminURL, groupNavItems } from '@payloadcms/ui/shared'
import LinkWithDefault from 'next/link.js'
import { usePathname } from 'next/navigation.js'
import React, { Fragment } from 'react'
const baseClass = 'nav'
@@ -21,6 +22,7 @@ const baseClass = 'nav'
export const DefaultNavClient: React.FC = () => {
const { permissions } = useAuth()
const { isEntityVisible } = useEntityVisibility()
const pathname = usePathname()
const {
collections,
@@ -84,17 +86,11 @@ export const DefaultNavClient: React.FC = () => {
LinkWithDefault) as typeof LinkWithDefault.default
const LinkElement = Link || 'a'
const activeCollection = window?.location?.pathname
?.split('/')
.find(
(_, index, arr) =>
arr[index - 1] === 'collections' || arr[index - 1] === 'globals',
)
const activeCollection = pathname.endsWith(href)
return (
<LinkElement
className={[`${baseClass}__link`, activeCollection === entity?.slug && `active`]
className={[`${baseClass}__link`, activeCollection && `active`]
.filter(Boolean)
.join(' ')}
href={href}
@@ -102,9 +98,11 @@ export const DefaultNavClient: React.FC = () => {
key={i}
tabIndex={!navOpen ? -1 : undefined}
>
<span className={`${baseClass}__link-icon`}>
<ChevronIcon direction="right" />
</span>
{activeCollection && (
<span className={`${baseClass}__link-icon`}>
<ChevronIcon direction="right" />
</span>
)}
<span className={`${baseClass}__link-label`}>{entityLabel}</span>
</LinkElement>
)

View File

@@ -110,16 +110,9 @@
&__link {
display: flex;
align-items: center;
&.active {
.nav__link-icon {
display: block;
}
}
}
&__link-icon {
display: none;
margin-right: calc(var(--base) * 0.25);
top: -1px;
position: relative;

View File

@@ -1,12 +1,13 @@
import type { AcceptedLanguages, I18nClient } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload'
import type { PayloadRequest, SanitizedConfig } from 'payload'
import { initI18n, rtlLanguages } from '@payloadcms/translations'
import { RootProvider } from '@payloadcms/ui'
import '@payloadcms/ui/scss/app.scss'
import { buildComponentMap } from '@payloadcms/ui/utilities/buildComponentMap'
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
import { createClientConfig, parseCookies } from 'payload'
import { createClientConfig, createLocalReq, parseCookies } from 'payload'
import * as qs from 'qs-esm'
import React from 'react'
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
@@ -52,6 +53,20 @@ export const RootLayout = async ({
language: languageCode,
})
const req = await createLocalReq(
{
fallbackLocale: null,
req: {
headers,
host: headers.get('host'),
i18n,
url: `${payload.config.serverURL}`,
} as PayloadRequest,
},
payload,
)
const { permissions, user } = await payload.auth({ headers, req })
const clientConfig = await createClientConfig({ config, t: i18n.t })
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
@@ -100,9 +115,11 @@ export const RootLayout = async ({
fallbackLang={clientConfig.i18n.fallbackLanguage}
languageCode={languageCode}
languageOptions={languageOptions}
permissions={permissions}
switchLanguageServerAction={switchLanguageServerAction}
theme={theme}
translations={i18n.translations}
user={user}
>
{wrappedChildren}
</RootProvider>

View File

@@ -1,6 +1,5 @@
import type { Metadata } from 'next'
import type { Icon } from 'next/dist/lib/metadata/types/metadata-types.js'
import type { MetaConfig } from 'payload'
import type { IconConfig, MetaConfig } from 'payload'
import { payloadFaviconDark, payloadFaviconLight, staticOGImage } from '@payloadcms/ui/assets'
import * as qs from 'qs-esm'
@@ -24,7 +23,7 @@ export const meta = async (args: { serverURL: string } & MetaConfig): Promise<an
titleSuffix,
} = args
const payloadIcons: Icon[] = [
const payloadIcons: IconConfig[] = [
{
type: 'image/png',
rel: 'icon',
@@ -40,10 +39,10 @@ export const meta = async (args: { serverURL: string } & MetaConfig): Promise<an
},
]
let icons = customIcons ?? payloadIcons // TODO: fix this type assertion
let icons = payloadIcons
if (customIcons && typeof customIcons === 'object' && Array.isArray(customIcons)) {
icons = payloadIcons.concat(customIcons) // TODO: fix this type assertion
icons = customIcons
}
const metaTitle = `${title} ${titleSuffix}`

View File

@@ -1,6 +1,6 @@
import type { AdminViewProps, ServerSideEditViewProps } from 'payload'
import { DocumentInfoProvider, HydrateClientUser } from '@payloadcms/ui'
import { DocumentInfoProvider, HydrateAuthProvider } from '@payloadcms/ui'
import { RenderCustomComponent } from '@payloadcms/ui/shared'
import { notFound } from 'next/navigation.js'
import React from 'react'
@@ -82,7 +82,7 @@ export const Account: React.FC<AdminViewProps> = async ({
i18n={i18n}
permissions={permissions}
/>
<HydrateClientUser permissions={permissions} user={user} />
<HydrateAuthProvider permissions={permissions} />
<RenderCustomComponent
CustomComponent={
typeof CustomAccountComponent === 'function' ? CustomAccountComponent : undefined

View File

@@ -15,7 +15,7 @@ import {
import { getFormState } from '@payloadcms/ui/shared'
import React from 'react'
import { EmailAndUsernameFields } from '../../elements/EmailAndUsername/index.js'
import { RenderEmailAndUsernameFields } from '../../elements/EmailAndUsername/index.js'
export const CreateFirstUserClient: React.FC<{
initialState: FormState
@@ -57,8 +57,14 @@ export const CreateFirstUserClient: React.FC<{
redirect={admin}
validationOperation="create"
>
<EmailAndUsernameFields loginWithUsername={loginWithUsername} />
<RenderEmailAndUsernameFields
className="emailAndUsername"
loginWithUsername={loginWithUsername}
operation="create"
readOnly={false}
/>
<PasswordField
autoComplete="off"
label={t('authentication:newPassword')}
name="password"
path="password"

View File

@@ -3,3 +3,7 @@
margin-bottom: var(--base);
}
}
.emailAndUsername {
margin-bottom: var(--base);
}

View File

@@ -1,7 +1,7 @@
import type { EntityToGroup } from '@payloadcms/ui/shared'
import type { AdminViewProps } from 'payload'
import { HydrateClientUser } from '@payloadcms/ui'
import { HydrateAuthProvider } from '@payloadcms/ui'
import { EntityType, RenderCustomComponent, groupNavItems } from '@payloadcms/ui/shared'
import LinkImport from 'next/link.js'
import React, { Fragment } from 'react'
@@ -79,7 +79,7 @@ export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult, params, se
return (
<Fragment>
<HydrateClientUser permissions={permissions} user={user} />
<HydrateAuthProvider permissions={permissions} />
<RenderCustomComponent
CustomComponent={
typeof CustomDashboardComponent === 'function' ? CustomDashboardComponent : undefined

View File

@@ -1,7 +1,11 @@
import type { AdminViewComponent, AdminViewProps, EditViewComponent } from 'payload'
import { DocumentInfoProvider, EditDepthProvider, HydrateClientUser } from '@payloadcms/ui'
import { RenderCustomComponent, formatAdminURL , isEditing as getIsEditing } from '@payloadcms/ui/shared'
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
import {
RenderCustomComponent,
formatAdminURL,
isEditing as getIsEditing,
} from '@payloadcms/ui/shared'
import { notFound, redirect } from 'next/navigation.js'
import React from 'react'
@@ -208,7 +212,15 @@ export const Document: React.FC<AdminViewProps> = async ({
permissions={permissions}
/>
)}
<HydrateClientUser permissions={permissions} user={user} />
<HydrateAuthProvider permissions={permissions} />
{/**
* After bumping the Next.js canary to 104, and React to 19.0.0-rc-06d0b89e-20240801" we have to deepCopy the permissions object (https://github.com/payloadcms/payload/pull/7541).
* If both HydrateClientUser and RenderCustomComponent receive the same permissions object (same object reference), we get a
* "TypeError: Cannot read properties of undefined (reading '$$typeof')" error when loading up some version views - for example a versions
* view in the draft-posts collection of the versions test suite. RenderCustomComponent is what renders the versions view.
*
* // TODO: Revisit this in the future and figure out why this is happening. Might be a React/Next.js bug. We don't know why it happens, and a future React/Next version might unbreak this (keep an eye on this and remove deepCopyObjectSimple if that's the case)
*/}
<EditDepthProvider
depth={1}
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}

View File

@@ -17,7 +17,7 @@ import { toast } from 'sonner'
import type { Props } from './types.js'
import { EmailAndUsernameFields } from '../../../../elements/EmailAndUsername/index.js'
import { RenderEmailAndUsernameFields } from '../../../../elements/EmailAndUsername/index.js'
import { APIKey } from './APIKey.js'
import './index.scss'
@@ -47,7 +47,7 @@ export const Auth: React.FC<Props> = (props) => {
const dispatchFields = useFormFields((reducer) => reducer[1])
const modified = useFormModified()
const { i18n, t } = useTranslation()
const { isInitializing } = useDocumentInfo()
const { docPermissions, isInitializing } = useDocumentInfo()
const {
routes: { api },
@@ -138,7 +138,12 @@ export const Auth: React.FC<Props> = (props) => {
<div className={[baseClass, className].filter(Boolean).join(' ')}>
{!disableLocalStrategy && (
<React.Fragment>
<EmailAndUsernameFields loginWithUsername={loginWithUsername} />
<RenderEmailAndUsernameFields
loginWithUsername={loginWithUsername}
operation={operation}
permissions={docPermissions?.fields}
readOnly={readOnly}
/>
{(showPasswordFields || requirePassword) && (
<div className={`${baseClass}__changing-password`}>
<PasswordField

View File

@@ -103,7 +103,15 @@ export const DefaultEditView: React.FC = () => {
const classes = [baseClass, id && `${baseClass}--is-editing`].filter(Boolean).join(' ')
const [schemaPath, setSchemaPath] = React.useState(entitySlug)
const [validateBeforeSubmit, setValidateBeforeSubmit] = useState(false)
const [validateBeforeSubmit, setValidateBeforeSubmit] = useState(() => {
if (
operation === 'create' &&
collectionConfig.auth &&
!collectionConfig.auth.disableLocalStrategy
)
return true
return false
})
const onSave = useCallback(
(json) => {

View File

@@ -1,7 +1,7 @@
import type { AdminViewProps, Where } from 'payload'
import {
HydrateClientUser,
HydrateAuthProvider,
ListInfoProvider,
ListQueryProvider,
TableColumnsProvider,
@@ -57,9 +57,23 @@ export const ListView: React.FC<AdminViewProps> = async ({
req,
user,
where: {
key: {
equals: preferenceKey,
},
and: [
{
key: {
equals: preferenceKey,
},
},
{
'user.relationTo': {
equals: user.collection,
},
},
{
'user.value': {
equals: user?.id,
},
},
],
},
})
?.then((res) => res?.docs?.[0]?.value)) as ListPreferences
@@ -124,7 +138,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
return (
<Fragment>
<HydrateClientUser permissions={permissions} user={user} />
<HydrateAuthProvider permissions={permissions} />
<ListInfoProvider
collectionConfig={createClientCollectionConfig({
collection: collectionConfig,

View File

@@ -45,13 +45,10 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true })
path="username"
required={required}
validate={(value, options) => {
const passesUsername = username(
value,
options as ValidateOptions<any, { email?: string }, any, string>,
)
const passesUsername = username(value, options)
const passesEmail = email(
value,
options as ValidateOptions<any, { username?: string }, any, string>,
options as ValidateOptions<any, { username?: string }, any, any>,
)
if (!passesEmail && !passesUsername) {

View File

@@ -2,7 +2,7 @@ import type { I18n } from '@payloadcms/translations'
import type { Metadata } from 'next'
import type { AdminViewComponent, SanitizedConfig } from 'payload'
import { HydrateClientUser } from '@payloadcms/ui'
import { HydrateAuthProvider } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared'
import React, { Fragment } from 'react'
@@ -58,21 +58,18 @@ export const NotFoundPage = async ({
})
return (
<Fragment>
<HydrateClientUser permissions={initPageResult.permissions} user={initPageResult.req.user} />
<DefaultTemplate
i18n={initPageResult.req.i18n}
locale={initPageResult.locale}
params={params}
payload={initPageResult.req.payload}
permissions={initPageResult.permissions}
searchParams={searchParams}
user={initPageResult.req.user}
visibleEntities={initPageResult.visibleEntities}
>
<NotFoundClient />
</DefaultTemplate>
</Fragment>
<DefaultTemplate
i18n={initPageResult.req.i18n}
locale={initPageResult.locale}
params={params}
payload={initPageResult.req.payload}
permissions={initPageResult.permissions}
searchParams={searchParams}
user={initPageResult.req.user}
visibleEntities={initPageResult.visibleEntities}
>
<NotFoundClient />
</DefaultTemplate>
)
}

View File

@@ -26,7 +26,13 @@ const generateLabelFromValue = (
locale: string,
value: { relationTo: string; value: RelationshipValue } | RelationshipValue,
): string => {
let relation: string
if (Array.isArray(value)) {
return value
.map((v) => generateLabelFromValue(collections, field, locale, v))
.filter(Boolean) // Filters out any undefined or empty values
.join(', ')
}
let relatedDoc: RelationshipValue
let valueToReturn = '' as any
@@ -37,17 +43,20 @@ const generateLabelFromValue = (
return String(value)
}
if (Array.isArray(relationTo)) {
if (typeof value === 'object') {
relation = value.relationTo
relatedDoc = value.value
}
if (typeof value === 'object' && 'relationTo' in value) {
relatedDoc = value.value
} else {
relation = relationTo
// Non-polymorphic relationship
relatedDoc = value
}
const relatedCollection = collections.find((c) => c.slug === relation)
const relatedCollection = relationTo
? collections.find(
(c) =>
c.slug ===
(typeof value === 'object' && 'relationTo' in value ? value.relationTo : relationTo),
)
: null
if (relatedCollection) {
const useAsTitle = relatedCollection?.admin?.useAsTitle
@@ -56,45 +65,65 @@ const generateLabelFromValue = (
)
let titleFieldIsLocalized = false
if (useAsTitleField && fieldAffectsData(useAsTitleField))
if (useAsTitleField && fieldAffectsData(useAsTitleField)) {
titleFieldIsLocalized = useAsTitleField.localized
}
if (typeof relatedDoc?.[useAsTitle] !== 'undefined') {
valueToReturn = relatedDoc[useAsTitle]
} else if (typeof relatedDoc?.id !== 'undefined') {
valueToReturn = relatedDoc.id
} else {
valueToReturn = relatedDoc
}
if (typeof valueToReturn === 'object' && titleFieldIsLocalized) {
valueToReturn = valueToReturn[locale]
}
} else if (relatedDoc) {
// Handle non-polymorphic `hasMany` relationships or fallback
if (typeof relatedDoc?.id !== 'undefined') {
valueToReturn = relatedDoc.id
} else {
valueToReturn = relatedDoc
}
}
if (typeof valueToReturn === 'object' && valueToReturn !== null) {
valueToReturn = JSON.stringify(valueToReturn)
}
return valueToReturn
}
const Relationship: React.FC<Props> = ({ comparison, field, i18n, locale, version }) => {
let placeholder = ''
const placeholder = `[${i18n.t('general:noValue')}]`
const { collections } = useConfig()
if (version === comparison) placeholder = `[${i18n.t('general:noValue')}]`
let versionToRender: string | undefined = placeholder
let comparisonToRender: string | undefined = placeholder
let versionToRender = version
let comparisonToRender = comparison
if (version) {
if ('hasMany' in field && field.hasMany && Array.isArray(version)) {
versionToRender =
version.map((val) => generateLabelFromValue(collections, field, locale, val)).join(', ') ||
placeholder
} else {
versionToRender = generateLabelFromValue(collections, field, locale, version) || placeholder
}
}
if ('hasMany' in field && field.hasMany) {
if (Array.isArray(version))
versionToRender = version
.map((val) => generateLabelFromValue(collections, field, locale, val))
.join(', ')
if (Array.isArray(comparison))
comparisonToRender = comparison
.map((val) => generateLabelFromValue(collections, field, locale, val))
.join(', ')
} else {
versionToRender = generateLabelFromValue(collections, field, locale, version)
comparisonToRender = generateLabelFromValue(collections, field, locale, comparison)
if (comparison) {
if ('hasMany' in field && field.hasMany && Array.isArray(comparison)) {
comparisonToRender =
comparison
.map((val) => generateLabelFromValue(collections, field, locale, val))
.join(', ') || placeholder
} else {
comparisonToRender =
generateLabelFromValue(collections, field, locale, comparison) || placeholder
}
}
const label =
@@ -112,10 +141,8 @@ const Relationship: React.FC<Props> = ({ comparison, field, i18n, locale, versio
</Label>
<ReactDiffViewer
hideLineNumbers
newValue={typeof versionToRender !== 'undefined' ? String(versionToRender) : placeholder}
oldValue={
typeof comparisonToRender !== 'undefined' ? String(comparisonToRender) : placeholder
}
newValue={versionToRender}
oldValue={comparisonToRender}
showDiffOnly={false}
splitView
styles={diffStyles}

View File

@@ -18,12 +18,12 @@ export default {
number: Text,
point: Text,
radio: Select,
relationship: null,
relationship: Relationship,
richText: Text,
row: Nested,
select: Select,
tabs: Tabs,
text: Text,
textarea: Text,
upload: null,
upload: Relationship,
}

View File

@@ -1,12 +1,12 @@
import type {
CollectionPermission,
Document,
EditViewComponent,
GlobalPermission,
OptionObject,
} from 'payload'
import { notFound } from 'next/navigation.js'
import {
type CollectionPermission,
type Document,
type EditViewComponent,
type GlobalPermission,
type OptionObject,
deepCopyObjectSimple,
} from 'payload'
import React from 'react'
import { getLatestVersion } from '../Versions/getLatestVersion.js'
@@ -55,8 +55,18 @@ export const VersionView: EditViewComponent = async (props) => {
})
if (collectionConfig?.versions?.drafts) {
latestDraftVersion = await getLatestVersion(payload, slug, 'draft', 'collection')
latestPublishedVersion = await getLatestVersion(payload, slug, 'published', 'collection')
latestDraftVersion = await getLatestVersion({
slug,
type: 'collection',
payload,
status: 'draft',
})
latestPublishedVersion = await getLatestVersion({
slug,
type: 'collection',
payload,
status: 'published',
})
}
} catch (error) {
return notFound()
@@ -80,8 +90,18 @@ export const VersionView: EditViewComponent = async (props) => {
})
if (globalConfig?.versions?.drafts) {
latestDraftVersion = await getLatestVersion(payload, slug, 'draft', 'global')
latestPublishedVersion = await getLatestVersion(payload, slug, 'published', 'global')
latestDraftVersion = await getLatestVersion({
slug,
type: 'global',
payload,
status: 'draft',
})
latestPublishedVersion = await getLatestVersion({
slug,
type: 'global',
payload,
status: 'published',
})
}
} catch (error) {
return notFound()
@@ -116,7 +136,14 @@ export const VersionView: EditViewComponent = async (props) => {
return (
<DefaultVersionView
doc={doc}
docPermissions={docPermissions}
/**
* After bumping the Next.js canary to 104, and React to 19.0.0-rc-06d0b89e-20240801" we have to deepCopy the permissions object (https://github.com/payloadcms/payload/pull/7541).
* If both HydrateClientUser and RenderCustomComponent receive the same permissions object (same object reference), we get a
* "TypeError: Cannot read properties of undefined (reading '$$typeof')" error
*
* // TODO: Revisit this in the future and figure out why this is happening. Might be a React/Next.js bug. We don't know why it happens, and a future React/Next version might unbreak this (keep an eye on this and remove deepCopyObjectSimple if that's the case)
*/
docPermissions={deepCopyObjectSimple(docPermissions)}
initialComparisonDoc={latestVersion}
latestDraftVersion={latestDraftVersion?.id}
latestPublishedVersion={latestPublishedVersion?.id}

View File

@@ -1,4 +1,19 @@
export async function getLatestVersion(payload, slug, status, type = 'collection') {
import type { Payload } from 'payload'
type ReturnType = {
id: string
updatedAt: string
} | null
type Args = {
payload: Payload
slug: string
status: 'draft' | 'published'
type: 'collection' | 'global'
}
export async function getLatestVersion(args: Args): Promise<ReturnType> {
const { slug, type = 'collection', payload, status } = args
try {
const sharedOptions = {
depth: 0,
@@ -22,11 +37,16 @@ export async function getLatestVersion(payload, slug, status, type = 'collection
...sharedOptions,
})
if (!response.docs.length) {
return null
}
return {
id: response.docs[0].id,
updatedAt: response.docs[0].updatedAt,
}
} catch (e) {
console.error(e)
return null
}
}

View File

@@ -62,13 +62,18 @@ export const VersionsView: EditViewComponent = async (props) => {
},
})
if (collectionConfig?.versions?.drafts) {
latestDraftVersion = await getLatestVersion(payload, collectionSlug, 'draft', 'collection')
latestPublishedVersion = await getLatestVersion(
latestDraftVersion = await getLatestVersion({
slug: collectionSlug,
type: 'collection',
payload,
collectionSlug,
'published',
'collection',
)
status: 'draft',
})
latestPublishedVersion = await getLatestVersion({
slug: collectionSlug,
type: 'collection',
payload,
status: 'published',
})
}
} catch (error) {
console.error(error) // eslint-disable-line no-console
@@ -90,8 +95,18 @@ export const VersionsView: EditViewComponent = async (props) => {
})
if (globalConfig?.versions?.drafts) {
latestDraftVersion = await getLatestVersion(payload, globalSlug, 'draft', 'global')
latestPublishedVersion = await getLatestVersion(payload, globalSlug, 'published', 'global')
latestDraftVersion = await getLatestVersion({
slug: globalSlug,
type: 'global',
payload,
status: 'draft',
})
latestPublishedVersion = await getLatestVersion({
slug: globalSlug,
type: 'global',
payload,
status: 'published',
})
}
} catch (error) {
console.error(error) // eslint-disable-line no-console

View File

@@ -18,21 +18,20 @@ export const withPayload = (nextConfig = {}) => {
env: {
...(nextConfig?.env || {}),
},
outputFileTracingExcludes: {
...(nextConfig?.outputFileTracingExcludes || {}),
'**/*': [
...(nextConfig?.outputFileTracingExcludes?.['**/*'] || []),
'drizzle-kit',
'drizzle-kit/api',
],
},
outputFileTracingIncludes: {
...(nextConfig?.outputFileTracingIncludes || {}),
'**/*': [...(nextConfig?.outputFileTracingIncludes?.['**/*'] || []), '@libsql/client'],
},
experimental: {
...(nextConfig?.experimental || {}),
outputFileTracingExcludes: {
'**/*': [
...(nextConfig.experimental?.outputFileTracingExcludes?.['**/*'] || []),
'drizzle-kit',
'drizzle-kit/api',
],
},
outputFileTracingIncludes: {
'**/*': [
...(nextConfig.experimental?.outputFileTracingIncludes?.['**/*'] || []),
'@libsql/client',
],
},
turbo: {
...(nextConfig?.experimental?.turbo || {}),
resolveAlias: {

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",
@@ -84,7 +84,7 @@
"pretest": "pnpm build"
},
"dependencies": {
"@next/env": "^15.0.0-canary.53",
"@next/env": "^15.0.0-canary.104",
"@payloadcms/translations": "workspace:*",
"@swc-node/core": "1.13.1",
"@swc-node/sourcemap-support": "0.5.0",

View File

@@ -6,7 +6,7 @@ import type { SanitizedCollectionConfig, TypeWithID } from '../collections/confi
import type { SanitizedConfig } from '../config/types.js'
import type { Field, FieldAffectingData, RichTextField, Validate } from '../fields/config/types.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import type { JsonObject, PayloadRequest, RequestContext } from '../types/index.js'
import type { JsonObject, Payload, PayloadRequest, RequestContext } from '../types/index.js'
import type { WithServerSidePropsComponentProps } from './elements/WithServerSideProps.js'
export type RichTextFieldProps<Value extends object, AdapterProps, ExtraFieldProperties = {}> = {
@@ -189,6 +189,7 @@ type RichTextAdapterBase<
WithServerSideProps: React.FC<Omit<WithServerSidePropsComponentProps, 'serverOnlyProps'>>
config: SanitizedConfig
i18n: I18nClient
payload: Payload
schemaPath: string
}) => Map<string, React.ReactNode>
generateSchemaMap?: (args: {

View File

@@ -15,7 +15,7 @@ export type ArrayFieldProps = {
name?: string
validate?: ArrayFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type ArrayFieldLabelComponent = LabelComponent<'array'>

View File

@@ -15,7 +15,7 @@ export type BlocksFieldProps = {
slug?: string
validate?: BlockFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type ReducedBlock = {
LabelComponent: Block['admin']['components']['Label']

View File

@@ -12,7 +12,7 @@ export type CheckboxFieldProps = {
path?: string
validate?: CheckboxFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type CheckboxFieldLabelComponent = LabelComponent<'checkbox'>

View File

@@ -10,7 +10,7 @@ export type CodeFieldProps = {
path?: string
validate?: CodeFieldValidation
width: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type CodeFieldLabelComponent = LabelComponent<'code'>

View File

@@ -10,7 +10,7 @@ export type DateFieldProps = {
placeholder?: DateField['admin']['placeholder'] | string
validate?: DateFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type DateFieldLabelComponent = LabelComponent<'date'>

View File

@@ -10,7 +10,7 @@ export type EmailFieldProps = {
placeholder?: EmailField['admin']['placeholder']
validate?: EmailFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type EmailFieldLabelComponent = LabelComponent<'email'>

View File

@@ -10,7 +10,7 @@ export type JSONFieldProps = {
path?: string
validate?: JSONFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type JSONFieldLabelComponent = LabelComponent<'json'>

View File

@@ -15,7 +15,7 @@ export type NumberFieldProps = {
step?: number
validate?: NumberFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type NumberFieldLabelComponent = LabelComponent<'number'>

View File

@@ -9,7 +9,7 @@ export type PointFieldProps = {
step?: number
validate?: PointFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type PointFieldLabelComponent = LabelComponent<'point'>

View File

@@ -12,7 +12,7 @@ export type RadioFieldProps = {
validate?: RadioFieldValidation
value?: string
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type OnChange<T = string> = (value: T) => void

View File

@@ -12,7 +12,7 @@ export type RelationshipFieldProps = {
sortOptions?: RelationshipField['admin']['sortOptions']
validate?: RelationshipFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type RelationshipFieldLabelComponent = LabelComponent<'relationship'>

View File

@@ -8,7 +8,7 @@ export type RichTextComponentProps = {
richTextComponentMap?: Map<string, MappedField[] | React.ReactNode>
validate?: RichTextFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type RichTextFieldLabelComponent = LabelComponent<'richText'>

View File

@@ -14,7 +14,7 @@ export type SelectFieldProps = {
validate?: SelectFieldValidation
value?: string
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type SelectFieldLabelComponent = LabelComponent<'select'>

View File

@@ -16,7 +16,7 @@ export type TextFieldProps = {
placeholder?: TextField['admin']['placeholder']
validate?: TextFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type TextFieldLabelComponent = LabelComponent<'text'>

View File

@@ -12,7 +12,7 @@ export type TextareaFieldProps = {
rows?: number
validate?: TextareaFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type TextareaFieldLabelComponent = LabelComponent<'textarea'>

View File

@@ -15,7 +15,7 @@ export type UploadFieldProps = {
relationTo?: UploadField['relationTo']
validate?: UploadFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type UploadFieldLabelComponent = LabelComponent<'upload'>

View File

@@ -2,9 +2,10 @@ import fs from 'fs'
import type { CreateMigration } from '../types.js'
import { writeMigrationIndex } from '../../index.js'
import { migrationTemplate } from './migrationTemplate.js'
export const createMigration: CreateMigration = async function createMigration({
export const createMigration: CreateMigration = function createMigration({
migrationName,
payload,
}) {
@@ -23,5 +24,8 @@ export const createMigration: CreateMigration = async function createMigration({
const fileName = `${timestamp}_${formattedName}.ts`
const filePath = `${dir}/${fileName}`
fs.writeFileSync(filePath, migrationTemplate)
writeMigrationIndex({ migrationsDir: payload.db.migrationDir })
payload.logger.info({ msg: `Migration created at ${filePath}` })
}

View File

@@ -7,9 +7,12 @@ import { killTransaction } from '../../utilities/killTransaction.js'
import { getMigrations } from './getMigrations.js'
import { readMigrationFiles } from './readMigrationFiles.js'
export async function migrate(this: BaseDatabaseAdapter): Promise<void> {
export const migrate: BaseDatabaseAdapter['migrate'] = async function migrate(
this: BaseDatabaseAdapter,
args,
): Promise<void> {
const { payload } = this
const migrationFiles = await readMigrationFiles({ payload })
const migrationFiles = args?.migrations || (await readMigrationFiles({ payload }))
const { existingMigrations, latestBatch } = await getMigrations({ payload })
const newBatch = latestBatch + 1

View File

@@ -27,7 +27,7 @@ export const readMigrationFiles = async ({
.readdirSync(payload.db.migrationDir)
.sort()
.filter((f) => {
return f.endsWith('.ts') || f.endsWith('.js')
return (f.endsWith('.ts') || f.endsWith('.js')) && !f.includes('index.')
})
.map((file) => {
return path.resolve(payload.db.migrationDir, file)

View File

@@ -0,0 +1,45 @@
import fs from 'fs'
import { getTsconfig } from 'get-tsconfig'
import path from 'path'
// Function to get all migration files (TS or JS) excluding 'index'
const getMigrationFiles = (dir: string) => {
return fs
.readdirSync(dir)
.filter(
(file) =>
(file.endsWith('.ts') || file.endsWith('.js')) &&
file !== 'index.ts' &&
file !== 'index.js',
)
.sort()
}
// Function to generate the index.ts content
const generateIndexContent = (files: string[]) => {
const tsconfig = getTsconfig()
const importExt = tsconfig?.config?.compilerOptions?.moduleResolution === 'NodeNext' ? '.js' : ''
let imports = ''
let exportsArray = 'export const migrations = [\n'
files.forEach((file, index) => {
const fileNameWithoutExt = file.replace(/\.[^/.]+$/, '')
imports += `import * as migration_${fileNameWithoutExt} from './${fileNameWithoutExt}${importExt}';\n`
exportsArray += ` {
up: migration_${fileNameWithoutExt}.up,
down: migration_${fileNameWithoutExt}.down,
name: '${fileNameWithoutExt}'${index !== files.length - 1 ? ',' : ''}\n },\n`
})
exportsArray += '];\n'
return imports + '\n' + exportsArray
}
// Main function to create the index.ts file
export const writeMigrationIndex = (args: { migrationsDir: string }) => {
const migrationFiles = getMigrationFiles(args.migrationsDir)
const indexContent = generateIndexContent(migrationFiles)
fs.writeFileSync(path.join(args.migrationsDir, 'index.ts'), indexContent)
}

View File

@@ -44,7 +44,6 @@ export interface BaseDatabaseAdapter {
deleteOne: DeleteOne
deleteVersions: DeleteVersions
/**
* Terminate the connection with the database
*/
@@ -68,7 +67,7 @@ export interface BaseDatabaseAdapter {
/**
* Run any migration up functions that have not yet been performed and update the status
*/
migrate: () => Promise<void>
migrate: (args?: { migrations?: Migration[] }) => Promise<void>
/**
* Run any migration down functions that have been performed
@@ -79,15 +78,16 @@ export interface BaseDatabaseAdapter {
* Drop the current database and run all migrate up functions
*/
migrateFresh: (args: { forceAcceptWarning?: boolean }) => Promise<void>
/**
* Run all migration down functions before running up
*/
migrateRefresh: () => Promise<void>
/**
* Run all migrate down functions
*/
migrateReset: () => Promise<void>
/**
* Read the current state of migrations and output the result to show which have been run
*/
@@ -148,7 +148,7 @@ export type CreateMigration = (args: {
forceAcceptWarning?: boolean
migrationName?: string
payload: Payload
}) => Promise<void>
}) => Promise<void> | void
export type Transaction = (
callback: () => Promise<void>,
@@ -396,8 +396,8 @@ export type DeleteManyArgs = {
export type DeleteMany = (args: DeleteManyArgs) => Promise<void>
export type Migration = {
down: ({ payload, req }: { payload: Payload; req: PayloadRequest }) => Promise<boolean>
up: ({ payload, req }: { payload: Payload; req: PayloadRequest }) => Promise<boolean>
down: (args: unknown) => Promise<void>
up: (args: unknown) => Promise<void>
} & MigrationData
export type MigrationData = {

View File

@@ -7,7 +7,6 @@ import type { CSSProperties } from 'react'
import monacoeditor from 'monaco-editor' // IMPORTANT - DO NOT REMOVE: This is required for pnpm's default isolated mode to work - even though the import is not used. This is due to a typescript bug: https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1519138189. (tsbugisolatedmode)
import type { JSONSchema4 } from 'json-schema'
import type React from 'react'
import type { DeepPartial } from 'ts-essentials'
import type { RichTextAdapter, RichTextAdapterProvider } from '../../admin/RichText.js'
import type { ErrorComponent } from '../../admin/forms/Error.js'
@@ -33,6 +32,10 @@ export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSibling
context: RequestContext
/** The data passed to update the document within create and update operations, and the full document itself in the afterRead hook. */
data?: Partial<TData>
/**
* Only available in the `afterRead` hook.
*/
draft?: boolean
/** The field which the hook is running against. */
field: FieldAffectingData
/** Boolean to denote if this hook is running against finding one, or finding many within the afterRead hook. */
@@ -60,6 +63,10 @@ export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSibling
* The schemaPath of the field, e.g. ["group", "myArray", "textField"]. The schemaPath is the path but without indexes and would be used in the context of field schemas, not field data.
*/
schemaPath: string[]
/**
* Only available in the `afterRead` hook.
*/
showHiddenFields?: boolean
/** The sibling data passed to a field that the hook is running against. */
siblingData: Partial<TSiblingData>
/**

View File

@@ -205,6 +205,7 @@ export const promise = async ({
collection,
context,
data: doc,
draft,
field,
findMany,
global,
@@ -214,6 +215,7 @@ export const promise = async ({
path: fieldPath,
req,
schemaPath: fieldSchemaPath,
showHiddenFields,
siblingData: siblingDoc,
value,
})
@@ -230,6 +232,7 @@ export const promise = async ({
collection,
context,
data: doc,
draft,
field,
findMany,
global,
@@ -239,6 +242,7 @@ export const promise = async ({
path: fieldPath,
req,
schemaPath: fieldSchemaPath,
showHiddenFields,
siblingData: siblingDoc,
value: siblingDoc[field.name],
})

View File

@@ -126,24 +126,25 @@ export const relationshipPopulationPromise = async ({
if (fieldSupportsMany(field) && field.hasMany) {
if (
field.localized &&
locale === 'all' &&
typeof siblingDoc[field.name] === 'object' &&
siblingDoc[field.name] !== null
) {
Object.keys(siblingDoc[field.name]).forEach((key) => {
if (Array.isArray(siblingDoc[field.name][key])) {
siblingDoc[field.name][key].forEach((relatedDoc, index) => {
Object.keys(siblingDoc[field.name]).forEach((localeKey) => {
if (Array.isArray(siblingDoc[field.name][localeKey])) {
siblingDoc[field.name][localeKey].forEach((relatedDoc, index) => {
const rowPromise = async () => {
await populate({
currentDepth,
data: siblingDoc[field.name][key][index],
data: siblingDoc[field.name][localeKey][index],
dataReference: resultingDoc,
depth: populateDepth,
draft,
fallbackLocale,
field,
index,
key,
key: localeKey,
locale,
overrideAccess,
req,
@@ -179,21 +180,22 @@ export const relationshipPopulationPromise = async ({
})
}
} else if (
field.localized &&
locale === 'all' &&
typeof siblingDoc[field.name] === 'object' &&
siblingDoc[field.name] !== null &&
locale === 'all'
siblingDoc[field.name] !== null
) {
Object.keys(siblingDoc[field.name]).forEach((key) => {
Object.keys(siblingDoc[field.name]).forEach((localeKey) => {
const rowPromise = async () => {
await populate({
currentDepth,
data: siblingDoc[field.name][key],
data: siblingDoc[field.name][localeKey],
dataReference: resultingDoc,
depth: populateDepth,
draft,
fallbackLocale,
field,
key,
key: localeKey,
locale,
overrideAccess,
req,

View File

@@ -777,6 +777,7 @@ export { migrateStatus } from './database/migrations/migrateStatus.js'
export { migrationTemplate } from './database/migrations/migrationTemplate.js'
export { migrationsCollection } from './database/migrations/migrationsCollection.js'
export { readMigrationFiles } from './database/migrations/readMigrationFiles.js'
export { writeMigrationIndex } from './database/migrations/writeMigrationIndex.js'
export type * from './database/queryValidation/types.js'
export type { EntityPolicies, PathToQuery } from './database/queryValidation/types.js'
export { validateQueryPaths } from './database/queryValidation/validateQueryPaths.js'

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",
@@ -62,8 +62,8 @@
},
"peerDependencies": {
"payload": "workspace:*",
"react": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610"
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801"
},
"publishConfig": {
"exports": {

View File

@@ -1,13 +1,15 @@
'use client'
import type { TextFieldProps } from 'payload'
import type { SelectFieldValidation, TextFieldProps } from 'payload'
import { SelectField, useForm } from '@payloadcms/ui'
import React, { useEffect, useState } from 'react'
import type { SelectFieldOption } from '../../types.js'
export const DynamicFieldSelector: React.FC<TextFieldProps> = (props) => {
export const DynamicFieldSelector: React.FC<
{ validate: SelectFieldValidation } & TextFieldProps
> = (props) => {
const { fields, getDataByPath } = useForm()
const [options, setOptions] = useState<SelectFieldOption[]>([])

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"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.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "Redirects plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-relationship-object-ids",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "A Payload plugin to store all relationship IDs as ObjectIDs",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-search",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "Search plugin for Payload",
"keywords": [
"payload",
@@ -55,8 +55,8 @@
},
"peerDependencies": {
"payload": "workspace:*",
"react": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610"
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801"
},
"publishConfig": {
"exports": {

View File

@@ -58,8 +58,8 @@
},
"peerDependencies": {
"payload": "workspace:*",
"react": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610"
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801"
},
"publishConfig": {
"exports": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-seo",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "SEO plugin for Payload",
"keywords": [
"payload",
@@ -64,8 +64,8 @@
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"payload": "workspace:*",
"react": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610"
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801"
},
"publishConfig": {
"exports": {

View File

@@ -54,7 +54,7 @@ export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
method: 'POST',
})
const { result: generatedImage } = await genImageResponse.json()
const generatedImage = await genImageResponse.text()
setValue(generatedImage || '')
}, [hasGenerateImageFn, docInfo, getData, locale, setValue])

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-stripe",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "Stripe plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "The officially supported Lexical richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -41,24 +41,24 @@
"translateNewKeys": "tsx scripts/translateNewKeys.ts"
},
"dependencies": {
"@lexical/headless": "0.16.1",
"@lexical/link": "0.16.1",
"@lexical/list": "0.16.1",
"@lexical/mark": "0.16.1",
"@lexical/markdown": "0.16.1",
"@lexical/react": "0.16.1",
"@lexical/rich-text": "0.16.1",
"@lexical/selection": "0.16.1",
"@lexical/utils": "0.16.1",
"@lexical/headless": "0.17.0",
"@lexical/link": "0.17.0",
"@lexical/list": "0.17.0",
"@lexical/mark": "0.17.0",
"@lexical/markdown": "0.17.0",
"@lexical/react": "0.17.0",
"@lexical/rich-text": "0.17.0",
"@lexical/selection": "0.17.0",
"@lexical/utils": "0.17.0",
"@types/uuid": "10.0.0",
"bson-objectid": "2.0.4",
"dequal": "2.0.3",
"lexical": "0.16.1",
"lexical": "0.17.0",
"react-error-boundary": "4.0.13",
"uuid": "10.0.0"
},
"devDependencies": {
"@lexical/eslint-plugin": " 0.16.1",
"@lexical/eslint-plugin": "0.17.0",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/next": "workspace:*",
"@payloadcms/translations": "workspace:*",
@@ -75,23 +75,23 @@
"peerDependencies": {
"@faceless-ui/modal": "3.0.0-beta.2",
"@faceless-ui/scroll-info": "2.0.0-beta.0",
"@lexical/headless": "0.16.1",
"@lexical/link": "0.16.1",
"@lexical/list": "0.16.1",
"@lexical/mark": "0.16.1",
"@lexical/markdown": "0.16.1",
"@lexical/react": "0.16.1",
"@lexical/rich-text": "0.16.1",
"@lexical/selection": "0.16.1",
"@lexical/table": "0.16.1",
"@lexical/utils": "0.16.1",
"@lexical/headless": "0.17.0",
"@lexical/link": "0.17.0",
"@lexical/list": "0.17.0",
"@lexical/mark": "0.17.0",
"@lexical/markdown": "0.17.0",
"@lexical/react": "0.17.0",
"@lexical/rich-text": "0.17.0",
"@lexical/selection": "0.17.0",
"@lexical/table": "0.17.0",
"@lexical/utils": "0.17.0",
"@payloadcms/next": "workspace:*",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"lexical": "0.16.1",
"lexical": "0.17.0",
"payload": "workspace:*",
"react": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610"
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801"
},
"engines": {
"node": "^18.20.2 || >=20.9.0"

View File

@@ -28,15 +28,26 @@ export const BlockquoteFeature = createServerFeature({
createNode({
converters: {
html: {
converter: async ({ converters, node, parent, req }) => {
converter: async ({
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
draft,
lexicalNodes: node.children,
overrideAccess,
parent: {
...node,
parent,
},
req,
showHiddenFields,
})
return `<blockquote>${childrenText}</blockquote>`

View File

@@ -5,15 +5,18 @@ import type { HTMLConverter } from '../types.js'
import { convertLexicalNodesToHTML } from '../index.js'
export const ParagraphHTMLConverter: HTMLConverter<SerializedParagraphNode> = {
async converter({ converters, node, parent, req }) {
async converter({ converters, draft, node, overrideAccess, parent, req, showHiddenFields }) {
const childrenText = await convertLexicalNodesToHTML({
converters,
draft,
lexicalNodes: node.children,
overrideAccess,
parent: {
...node,
parent,
},
req,
showHiddenFields,
})
return `<p>${childrenText}</p>`
},

View File

@@ -8,6 +8,9 @@ import type { HTMLConverter, SerializedLexicalNodeWithParent } from './types.js'
export type ConvertLexicalToHTMLArgs = {
converters: HTMLConverter[]
data: SerializedEditorState
draft?: boolean // default false
overrideAccess?: boolean // default false
showHiddenFields?: boolean // default false
} & (
| {
/**
@@ -40,8 +43,11 @@ export type ConvertLexicalToHTMLArgs = {
export async function convertLexicalToHTML({
converters,
data,
draft,
overrideAccess,
payload,
req,
showHiddenFields,
}: ConvertLexicalToHTMLArgs): Promise<string> {
if (data?.root?.children?.length) {
if (req === undefined && payload) {
@@ -50,9 +56,12 @@ export async function convertLexicalToHTML({
return await convertLexicalNodesToHTML({
converters,
draft: draft === undefined ? false : draft,
lexicalNodes: data?.root?.children,
overrideAccess: overrideAccess === undefined ? false : overrideAccess,
parent: data?.root,
req,
showHiddenFields: showHiddenFields === undefined ? false : showHiddenFields,
})
}
return ''
@@ -60,17 +69,23 @@ export async function convertLexicalToHTML({
export async function convertLexicalNodesToHTML({
converters,
draft,
lexicalNodes,
overrideAccess,
parent,
req,
showHiddenFields,
}: {
converters: HTMLConverter[]
draft: boolean
lexicalNodes: SerializedLexicalNode[]
overrideAccess: boolean
parent: SerializedLexicalNodeWithParent
/**
* When the converter is called, req CAN be passed in depending on where it's run.
*/
req: PayloadRequest | null
showHiddenFields: boolean
}): Promise<string> {
const unknownConverter = converters.find((converter) => converter.nodeTypes.includes('unknown'))
@@ -85,9 +100,12 @@ export async function convertLexicalNodesToHTML({
return await unknownConverter.converter({
childIndex: i,
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
})
}
return '<span>unknown node</span>'
@@ -95,9 +113,12 @@ export async function convertLexicalNodesToHTML({
return await converterForNode.converter({
childIndex: i,
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
})
} catch (error) {
console.error('Error converting lexical node to HTML:', error, 'node:', node)

View File

@@ -1,22 +1,19 @@
import type { SerializedLexicalNode } from 'lexical'
import type { Payload, PayloadRequest } from 'payload'
import type { PayloadRequest } from 'payload'
export type HTMLConverter<T extends SerializedLexicalNode = SerializedLexicalNode> = {
converter: ({
childIndex,
converters,
node,
parent,
req,
}: {
converter: (args: {
childIndex: number
converters: HTMLConverter[]
converters: HTMLConverter<any>[]
draft: boolean
node: T
overrideAccess: boolean
parent: SerializedLexicalNodeWithParent
/**
* When the converter is called, req CAN be passed in depending on where it's run.
*/
req: PayloadRequest | null
showHiddenFields: boolean
}) => Promise<string> | string
nodeTypes: string[]
}

View File

@@ -4,8 +4,8 @@ import { createServerFeature } from '../../../utilities/createServerFeature.js'
export type HTMLConverterFeatureProps = {
converters?:
| (({ defaultConverters }: { defaultConverters: HTMLConverter[] }) => HTMLConverter[])
| HTMLConverter[]
| (({ defaultConverters }: { defaultConverters: HTMLConverter<any>[] }) => HTMLConverter<any>[])
| HTMLConverter<any>[]
}
// This is just used to save the props on the richText field

View File

@@ -160,7 +160,16 @@ export const lexicalHTML: (
},
hooks: {
afterRead: [
async ({ collection, field, global, req, siblingData }) => {
async ({
collection,
draft,
field,
global,
overrideAccess,
req,
showHiddenFields,
siblingData,
}) => {
const fields = collection ? collection.fields : global.fields
const foundSiblingFields = findFieldPathAndSiblingFields(fields, [], field)
@@ -209,7 +218,10 @@ export const lexicalHTML: (
return await convertLexicalToHTML({
converters: finalConverters,
data: lexicalFieldData,
draft,
overrideAccess,
req,
showHiddenFields,
})
},
],

View File

@@ -8,6 +8,7 @@ import { slashMenuBasicGroupWithItems } from '../shared/slashMenu/basicGroup.js'
import { toolbarAddDropdownGroupWithItems } from '../shared/toolbar/addDropdownGroup.js'
import { TableActionMenuPlugin } from './plugins/TableActionMenuPlugin/index.js'
import { TableCellResizerPlugin } from './plugins/TableCellResizerPlugin/index.js'
import { TableHoverActionsPlugin } from './plugins/TableHoverActionsPlugin/index.js'
import {
OPEN_TABLE_DRAWER_COMMAND,
TableContext,
@@ -29,6 +30,10 @@ export const TableFeatureClient = createClientFeature({
Component: TableActionMenuPlugin,
position: 'floatingAnchorElem',
},
{
Component: TableHoverActionsPlugin,
position: 'floatingAnchorElem',
},
],
providers: [TableContext],
slashMenu: {

View File

@@ -1,3 +1,9 @@
import type {
SerializedTableCellNode as _SerializedTableCellNode,
SerializedTableNode as _SerializedTableNode,
SerializedTableRowNode as _SerializedTableRowNode,
} from '@lexical/table'
import type { Spread } from 'lexical'
import type { Config, Field } from 'payload'
import { TableCellNode, TableNode, TableRowNode } from '@lexical/table'
@@ -6,6 +12,8 @@ import { sanitizeFields } from 'payload'
// eslint-disable-next-line payload/no-imports-from-exports-dir
import { TableFeatureClient } from '../../exports/client/index.js'
import { createServerFeature } from '../../utilities/createServerFeature.js'
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
import { createNode } from '../typeUtilities.js'
const fields: Field[] = [
{
@@ -21,6 +29,27 @@ const fields: Field[] = [
required: true,
},
]
export type SerializedTableCellNode = Spread<
{
type: 'tablecell'
},
_SerializedTableCellNode
>
export type SerializedTableNode = Spread<
{
type: 'table'
},
_SerializedTableNode
>
export type SerializedTableRowNode = Spread<
{
type: 'tablerow'
},
_SerializedTableRowNode
>
export const EXPERIMENTAL_TableFeature = createServerFeature({
feature: async ({ config, isRoot }) => {
const validRelationships = config.collections.map((c) => c.slug) || []
@@ -41,15 +70,108 @@ export const EXPERIMENTAL_TableFeature = createServerFeature({
return schemaMap
},
nodes: [
{
createNode({
converters: {
html: {
converter: async ({
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
draft,
lexicalNodes: node.children,
overrideAccess,
parent: {
...node,
parent,
},
req,
showHiddenFields,
})
return `<table class="lexical-table" style="border-collapse: collapse;">${childrenText}</table>`
},
nodeTypes: [TableNode.getType()],
},
},
node: TableNode,
},
{
}),
createNode({
converters: {
html: {
converter: async ({
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
draft,
lexicalNodes: node.children,
overrideAccess,
parent: {
...node,
parent,
},
req,
showHiddenFields,
})
const tagName = node.headerState > 0 ? 'th' : 'td'
const headerStateClass = `lexical-table-cell-header-${node.headerState}`
const backgroundColor = node.backgroundColor
? `background-color: ${node.backgroundColor};`
: ''
const colSpan = node.colSpan > 1 ? `colspan="${node.colSpan}"` : ''
const rowSpan = node.rowSpan > 1 ? `rowspan="${node.rowSpan}"` : ''
return `<${tagName} class="lexical-table-cell ${headerStateClass}" style="border: 1px solid #ccc; padding: 8px; ${backgroundColor}" ${colSpan} ${rowSpan}>${childrenText}</${tagName}>`
},
nodeTypes: [TableCellNode.getType()],
},
},
node: TableCellNode,
},
{
}),
createNode({
converters: {
html: {
converter: async ({
converters,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
draft,
lexicalNodes: node.children,
overrideAccess,
parent: {
...node,
parent,
},
req,
showHiddenFields,
})
return `<tr class="lexical-table-row">${childrenText}</tr>`
},
nodeTypes: [TableRowNode.getType()],
},
},
node: TableRowNode,
},
}),
],
}
},

View File

@@ -9,9 +9,6 @@
.table-cell-action-button {
background-color: var(--theme-elevation-200);
display: flex;
justify-content: center;
align-items: center;
border: 0;
padding: 2px;
position: relative;

View File

@@ -160,15 +160,19 @@ function TableActionMenu({
const { y } = useScrollInfo()
useEffect(() => {
return editor.registerMutationListener(TableCellNode, (nodeMutations) => {
const nodeUpdated = nodeMutations.get(tableCellNode.getKey()) === 'updated'
return editor.registerMutationListener(
TableCellNode,
(nodeMutations) => {
const nodeUpdated = nodeMutations.get(tableCellNode.getKey()) === 'updated'
if (nodeUpdated) {
editor.getEditorState().read(() => {
updateTableCellNode(tableCellNode.getLatest())
})
}
})
if (nodeUpdated) {
editor.getEditorState().read(() => {
updateTableCellNode(tableCellNode.getLatest())
})
}
},
{ skipInitialization: true },
)
}, [editor, tableCellNode])
useEffect(() => {

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