Merge branch 'beta' of https://github.com/payloadcms/payload into fix/zhTW-translations

This commit is contained in:
PatrikKozak
2024-04-17 10:54:40 -04:00
571 changed files with 21370 additions and 31961 deletions

38
.github/CODEOWNERS vendored
View File

@@ -1,41 +1,33 @@
# Order matters. The last matching pattern takes precedence.
### Core ###
/packages/payload/src/uploads/ @denolfe
/packages/payload/src/admin/ @jmikrut @jacobsfletch @JarrodMFlesch
### Package Exports ###
/**/exports/ @denolfe @jmikrut
### Adapters ###
/packages/db-*/ @denolfe @jmikrut @DanRibbens
/packages/richtext-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
/packages/richtext-*/ @AlessioGr
### Plugins ###
/packages/plugin-*/ @denolfe @jmikrut @DanRibbens
/packages/plugin-cloud*/ @denolfe
/packages/plugin-form-builder/ @jacobsfletch
/packages/plugin-live-preview*/ @jacobsfletch
/packages/plugin-nested-docs/ @jacobsfletch
/packages/plugin-redirects/ @jacobsfletch
/packages/plugin-search/ @jacobsfletch
/packages/plugin-sentry/ @JessChowdhury
/packages/plugin-seo/ @jacobsfletch
/packages/plugin-stripe/ @jacobsfletch
### Examples ###
/examples/ @jacobsfletch
/examples/testing/ @JarrodMFlesch
/examples/email/ @JessChowdhury
/examples/whitelabel/ @JessChowdhury
### Templates ###
/templates/ @jacobsfletch @denolfe
### Misc ###
/packages/create-payload-app/ @denolfe
/packages/eslint-config-payload/ @denolfe
/packages/payload-admin-bar/ @jacobsfletch
/packages/eslint-*/ @denolfe
### Build Files ###
/**/package.json @denolfe
/tsconfig.json @denolfe
/**/tsconfig*.json @denolfe
/jest.config.js @denolfe
/**/jest.config.js @denolfe
### Root ###
/package.json @denolfe
/scripts/ @denolfe
/.husky/ @denolfe
/.vscode/ @denolfe
/.github/ @denolfe
/.github/CODEOWNERS @denolfe

View File

@@ -4,12 +4,15 @@ on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches: ['main', 'alpha']
branches: ['main', 'beta']
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: 18.20.2
jobs:
changes:
runs-on: ubuntu-latest
@@ -19,6 +22,10 @@ jobs:
needs_build: ${{ steps.filter.outputs.needs_build }}
templates: ${{ steps.filter.outputs.templates }}
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- uses: actions/checkout@v4
with:
fetch-depth: 25
@@ -49,10 +56,14 @@ jobs:
with:
fetch-depth: 25
- name: Use Node.js 18
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: 18
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v3
@@ -88,13 +99,16 @@ jobs:
tests-unit:
runs-on: ubuntu-latest
needs: build
if: false # Disable until tests are updated for 3.0
steps:
- name: Use Node.js 18
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: 18
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v3
@@ -136,10 +150,14 @@ jobs:
AWS_REGION: us-east-1
steps:
- name: Use Node.js 18
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: 18
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v3
@@ -221,8 +239,11 @@ jobs:
- email
- field-error-states
- fields-relationship
# - fields
- fields/lexical
- fields
- fields__collections__Blocks
- fields__collections__Array
- fields__collections__Relationship
- fields__collections__Lexical
- live-preview
- localization
- plugin-form-builder
@@ -232,10 +253,14 @@ jobs:
- uploads
steps:
- name: Use Node.js 18
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: 18
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v3
@@ -269,10 +294,14 @@ jobs:
needs: build
steps:
- name: Use Node.js 18
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: 18
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v3
@@ -306,11 +335,14 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 25
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Use Node.js 18
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: 18
node-version: ${{ env.NODE_VERSION }}
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.10.0

View File

@@ -1 +1 @@
v18.19.1
v18.20.2

2
.nvmrc
View File

@@ -1 +1 @@
v18.19.1
v18.20.2

23
.vscode/launch.json vendored
View File

@@ -41,6 +41,13 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js auth",
"cwd": "${workspaceFolder}",
"name": "Run Dev Auth",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev plugin-cloud-storage",
"cwd": "${workspaceFolder}",
@@ -69,36 +76,26 @@
}
},
{
"command": "pnpm run dev versions",
"command": "node --no-deprecation test/dev.js versions",
"cwd": "${workspaceFolder}",
"name": "Run Dev Versions",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev localization",
"command": "node --no-deprecation test/dev.js localization",
"cwd": "${workspaceFolder}",
"name": "Run Dev Localization",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev uploads",
"command": "node --no-deprecation test/dev.js uploads",
"cwd": "${workspaceFolder}",
"name": "Run Dev Uploads",
"request": "launch",
"type": "node-terminal"
},
{
"command": "PAYLOAD_BUNDLER=vite pnpm run dev fields",
"cwd": "${workspaceFolder}",
"name": "Run Dev Fields (Vite)",
"request": "launch",
"type": "node-terminal",
"env": {
"NODE_ENV": "production"
}
},
{
"command": "pnpm run test:int live-preview",
"cwd": "${workspaceFolder}",

View File

@@ -3,7 +3,7 @@ import type { Metadata } from 'next'
import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views/NotFound/index.js'
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
type Args = {
params: {
@@ -14,8 +14,8 @@ type Args = {
}
}
export const generateMetadata = ({ params }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params })
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })

View File

@@ -3,7 +3,7 @@ import type { Metadata } from 'next'
import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { RootPage, generatePageMetadata } from '@payloadcms/next/views/Root/index.js'
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
type Args = {
params: {

View File

@@ -1,7 +1,7 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST } from '@payloadcms/next/routes/index.js'
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)

View File

@@ -1,6 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes/index.js'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
export const GET = GRAPHQL_PLAYGROUND_GET(config)

View File

@@ -1,6 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY it because it could be re-written at any time. */
import config from '@payload-config'
import { GRAPHQL_POST } from '@payloadcms/next/routes/index.js'
import { GRAPHQL_POST } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)

View File

@@ -1,6 +1,6 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import configPromise from '@payload-config'
import { RootLayout } from '@payloadcms/next/layouts/Root/index.js'
import { RootLayout } from '@payloadcms/next/layouts'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import React from 'react'

View File

@@ -138,7 +138,7 @@ import { CallToAction } from '../blocks/CallToAction'
Here's an overview of all the included features:
| Feature Name | Included by default | Description |
| ------------------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|--------------------------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`BoldTextFeature`** | Yes | Handles the bold text format |
| **`ItalicTextFeature`** | Yes | Handles the italic text format |
| **`UnderlineTextFeature`** | Yes | Handles the underline text format |
@@ -157,7 +157,8 @@ Here's an overview of all the included features:
| **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents |
| **`BlockQuoteFeature`** | Yes | Allows you to create block-level quotes |
| **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an <hr> element |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. |
| **`TreeViewFeature`** | No | Adds a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging |
## Creating your own, custom Feature
@@ -234,6 +235,19 @@ This method employs `convertLexicalToHTML` from `@payloadcms/richtext-lexical`,
Because every `Feature` is able to provide html converters, and because the `htmlFeature` can modify those or provide their own, we need to consolidate them with the default html Converters using the `consolidateHTMLConverters` function.
#### CSS
Payload's lexical HTML converter does not generate CSS for you, but it does add classes to the generated HTML. You can use these classes to style the HTML in your frontend.
Here is some "base" CSS you can use to ensure that nested lists render correctly:
```css
/* Base CSS for Lexical HTML */
.nestedListItem, .list-check {
list-style-type: none;
}
```
#### Creating your own HTML Converter
HTML Converters are typed as `HTMLConverter`, which contains the node type it should handle, and a function that accepts the serialized node from the lexical editor, and outputs the HTML string. Here's the HTML Converter of the Upload node as an example:

View File

@@ -40,21 +40,22 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
### Collection Upload Options
| Option | Description |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`staticURL`** \* | The URL path to use to access your uploads. Relative path like `/media` will be served by payload. Full path like `https://example.com/media` needs to be served by another web server. |
| **`staticDir`** \* | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. |
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
| Option | Description |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`staticURL`** \* | The URL path to use to access your uploads. Relative path like `/media` will be served by payload. Full path like `https://example.com/media` needs to be served by another web server. |
| **`staticDir`** \* | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. |
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the Admin panel. Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`externalFileHeaderFilter`** | Accepts existing headers and can filter/modify them. |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the Admin panel. The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`handlers`** | Array of Express request handlers to execute before the built-in Payload static middleware executes. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
_An asterisk denotes that a property above is required._

View File

@@ -1 +0,0 @@
export default {}

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-alpha.59",
"version": "3.0.0-beta.11",
"private": true,
"type": "module",
"workspaces:": [
@@ -127,7 +127,7 @@
"lint-staged": "^14.0.1",
"minimist": "1.2.8",
"mongodb-memory-server": "^9.0",
"next": "14.2.0-canary.22",
"next": "^14.2.0-canary.23",
"node-mocks-http": "^1.14.1",
"nodemon": "3.0.3",
"open": "^10.1.0",
@@ -163,7 +163,7 @@
"react": "18.2.0"
},
"engines": {
"node": ">=18.17.0",
"node": ">=18.20.2",
"pnpm": ">=8"
},
"lint-staged": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-alpha.59",
"version": "3.0.0-beta.11",
"license": "MIT",
"type": "module",
"homepage": "https://payloadcms.com",
@@ -35,7 +35,7 @@
"comment-json": "^4.2.3",
"degit": "^2.8.4",
"detect-package-manager": "^3.0.1",
"esprima": "^4.0.1",
"esprima-next": "^6.0.3",
"execa": "^5.0.0",
"figures": "^6.1.0",
"fs-extra": "^9.0.1",

View File

@@ -4,6 +4,7 @@ import * as p from '@clack/prompts'
import { parse, stringify } from 'comment-json'
import execa from 'execa'
import fs from 'fs'
import fse from 'fs-extra'
import globby from 'globby'
import path from 'path'
import { promisify } from 'util'
@@ -31,6 +32,8 @@ type InitNextArgs = Pick<CliArgs, '--debug'> & {
useDistFiles?: boolean
}
type NextConfigType = 'cjs' | 'esm'
type InitNextResult =
| {
isSrcDir: boolean
@@ -45,11 +48,22 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const nextAppDetails = args.nextAppDetails || (await getNextAppDetails(projectDir))
const { hasTopLevelLayout, isSrcDir, nextAppDir } =
nextAppDetails || (await getNextAppDetails(projectDir))
if (!nextAppDetails.nextAppDir) {
warning(`Could not find app directory in ${projectDir}, creating...`)
const createdAppDir = path.resolve(projectDir, nextAppDetails.isSrcDir ? 'src/app' : 'app')
fse.mkdirSync(createdAppDir, { recursive: true })
nextAppDetails.nextAppDir = createdAppDir
}
if (!nextAppDir) {
return { isSrcDir, reason: `Could not find app directory in ${projectDir}`, success: false }
const { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigType } = nextAppDetails
if (!nextConfigType) {
return {
isSrcDir,
nextAppDir,
reason: `Could not determine Next Config type in ${projectDir}. Possibly try renaming next.config.js to next.config.cjs or next.config.mjs.`,
success: false,
}
}
if (hasTopLevelLayout) {
@@ -69,6 +83,7 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const configurationResult = installAndConfigurePayload({
...args,
nextAppDetails,
nextConfigType,
useDistFiles: true, // Requires running 'pnpm pack-template-files' in cpa
})
@@ -96,12 +111,23 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean) {
const tsConfigPath = path.resolve(projectDir, 'tsconfig.json')
// Check if tsconfig.json exists
if (!fs.existsSync(tsConfigPath)) {
warning(`Could not find tsconfig.json to add @payload-config path.`)
return
}
const userTsConfigContent = await readFile(tsConfigPath, {
encoding: 'utf8',
})
const userTsConfig = parse(userTsConfigContent) as {
compilerOptions?: CompilerOptions
}
const hasBaseUrl =
userTsConfig?.compilerOptions?.baseUrl && userTsConfig?.compilerOptions?.baseUrl !== '.'
const baseUrl = hasBaseUrl ? userTsConfig?.compilerOptions?.baseUrl : './'
if (!userTsConfig.compilerOptions && !('extends' in userTsConfig)) {
userTsConfig.compilerOptions = {}
}
@@ -112,20 +138,25 @@ async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean)
) {
userTsConfig.compilerOptions.paths = {
...(userTsConfig.compilerOptions.paths || {}),
'@payload-config': [`./${isSrcDir ? 'src/' : ''}payload.config.ts`],
'@payload-config': [`${baseUrl}${isSrcDir ? 'src/' : ''}payload.config.ts`],
}
await writeFile(tsConfigPath, stringify(userTsConfig, null, 2), { encoding: 'utf8' })
}
}
function installAndConfigurePayload(
args: InitNextArgs & { nextAppDetails: NextAppDetails; useDistFiles?: boolean },
args: InitNextArgs & {
nextAppDetails: NextAppDetails
nextConfigType: NextConfigType
useDistFiles?: boolean
},
):
| { payloadConfigPath: string; success: true }
| { payloadConfigPath?: string; reason: string; success: false } {
const {
'--debug': debug,
nextAppDetails: { isSrcDir, nextAppDir, nextConfigPath } = {},
nextConfigType,
projectDir,
useDistFiles,
} = args
@@ -172,6 +203,7 @@ function installAndConfigurePayload(
logDebug(`nextAppDir: ${nextAppDir}`)
logDebug(`projectDir: ${projectDir}`)
logDebug(`nextConfigPath: ${nextConfigPath}`)
logDebug(`payloadConfigPath: ${path.resolve(projectDir, 'payload.config.ts')}`)
logDebug(
`isSrcDir: ${isSrcDir}. source: ${templateSrcDir}. dest: ${path.dirname(nextConfigPath)}`,
@@ -181,7 +213,7 @@ function installAndConfigurePayload(
copyRecursiveSync(templateSrcDir, path.dirname(nextConfigPath), debug)
// Wrap next.config.js with withPayload
wrapNextConfig({ nextConfigPath })
wrapNextConfig({ nextConfigPath, nextConfigType })
return {
payloadConfigPath: path.resolve(nextAppDir, '../payload.config.ts'),
@@ -191,10 +223,10 @@ function installAndConfigurePayload(
async function installDeps(projectDir: string, packageManager: PackageManager, dbType: DbType) {
const packagesToInstall = ['payload', '@payloadcms/next', '@payloadcms/richtext-lexical'].map(
(pkg) => `${pkg}@alpha`,
(pkg) => `${pkg}@beta`,
)
packagesToInstall.push(`@payloadcms/db-${dbType}@alpha`)
packagesToInstall.push(`@payloadcms/db-${dbType}@beta`)
let exitCode = 0
switch (packageManager) {
@@ -226,6 +258,7 @@ type NextAppDetails = {
isSrcDir: boolean
nextAppDir?: string
nextConfigPath?: string
nextConfigType?: NextConfigType
}
export async function getNextAppDetails(projectDir: string): Promise<NextAppDetails> {
@@ -246,6 +279,7 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
await globby(['**/app'], {
absolute: true,
cwd: projectDir,
ignore: ['**/node_modules/**'],
onlyDirectories: true,
})
)?.[0]
@@ -254,9 +288,31 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
nextAppDir = undefined
}
const configType = await getProjectType(projectDir, nextConfigPath)
const hasTopLevelLayout = nextAppDir
? fs.existsSync(path.resolve(nextAppDir, 'layout.tsx'))
: false
return { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigPath }
return { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigPath, nextConfigType: configType }
}
async function getProjectType(projectDir: string, nextConfigPath: string): Promise<'cjs' | 'esm'> {
if (nextConfigPath.endsWith('.mjs')) {
return 'esm'
}
if (nextConfigPath.endsWith('.cjs')) {
return 'cjs'
}
const packageObj = await fse.readJson(path.resolve(projectDir, 'package.json'))
const packageJsonType = packageObj.type
if (packageJsonType === 'module') {
return 'esm'
}
if (packageJsonType === 'commonjs') {
return 'cjs'
}
return 'cjs'
}

View File

@@ -10,14 +10,18 @@ const mongodbReplacement: DbAdapterReplacement = {
importReplacement: "import { mongooseAdapter } from '@payloadcms/db-mongodb'",
packageName: '@payloadcms/db-mongodb',
// Replacement between `// database-adapter-config-start` and `// database-adapter-config-end`
configReplacement: [' db: mongooseAdapter({', ' url: process.env.DATABASE_URI,', ' }),'],
configReplacement: [
' db: mongooseAdapter({',
" url: process.env.DATABASE_URI || '',",
' }),',
],
}
const postgresReplacement: DbAdapterReplacement = {
configReplacement: [
' db: postgresAdapter({',
' pool: {',
' connectionString: process.env.DATABASE_URI,',
" connectionString: process.env.DATABASE_URI || '',",
' },',
' }),',
],

View File

@@ -16,7 +16,7 @@ const dbChoiceRecord: Record<DbType, DbChoice> = {
value: 'mongodb',
},
postgres: {
dbConnectionPrefix: 'postgres://127.0.0.1:5432/',
dbConnectionPrefix: 'postgres://postgres:<password>@127.0.0.1:5432/',
title: 'PostgreSQL (beta)',
value: 'postgres',
},

View File

@@ -18,43 +18,46 @@ export function getValidTemplates(): ProjectTemplate[] {
name: 'blank-3.0',
type: 'starter',
description: 'Blank 3.0 Template',
url: 'https://github.com/payloadcms/payload/templates/blank-3.0',
},
{
name: 'blank',
type: 'starter',
description: 'Blank Template',
url: 'https://github.com/payloadcms/payload/templates/blank',
},
{
name: 'website',
type: 'starter',
description: 'Website Template',
url: 'https://github.com/payloadcms/payload/templates/website',
},
{
name: 'ecommerce',
type: 'starter',
description: 'E-commerce Template',
url: 'https://github.com/payloadcms/payload/templates/ecommerce',
url: 'https://github.com/payloadcms/payload/templates/blank-3.0#beta',
},
// Remove these until they have been updated for 3.0
// {
// name: 'blank',
// type: 'starter',
// description: 'Blank Template',
// url: 'https://github.com/payloadcms/payload/templates/blank',
// },
// {
// name: 'website',
// type: 'starter',
// description: 'Website Template',
// url: 'https://github.com/payloadcms/payload/templates/website',
// },
// {
// name: 'ecommerce',
// type: 'starter',
// description: 'E-commerce Template',
// url: 'https://github.com/payloadcms/payload/templates/ecommerce',
// },
{
name: 'plugin',
type: 'plugin',
description: 'Template for creating a Payload plugin',
url: 'https://github.com/payloadcms/payload-plugin-template',
},
{
name: 'payload-demo',
type: 'starter',
description: 'Payload demo site at https://demo.payloadcms.com',
url: 'https://github.com/payloadcms/public-demo',
},
{
name: 'payload-website',
type: 'starter',
description: 'Payload website CMS at https://payloadcms.com',
url: 'https://github.com/payloadcms/website-cms',
url: 'https://github.com/payloadcms/payload-plugin-template#beta',
},
// {
// name: 'payload-demo',
// type: 'starter',
// description: 'Payload demo site at https://demo.payloadcms.com',
// url: 'https://github.com/payloadcms/public-demo',
// },
// {
// name: 'payload-website',
// type: 'starter',
// description: 'Payload website CMS at https://payloadcms.com',
// url: 'https://github.com/payloadcms/website-cms',
// },
]
}

View File

@@ -1,61 +1,159 @@
import { parseAndModifyConfigContent, withPayloadImportStatement } from './wrap-next-config.js'
import { parseAndModifyConfigContent, withPayloadStatement } from './wrap-next-config.js'
import * as p from '@clack/prompts'
const defaultNextConfig = `/** @type {import('next').NextConfig} */
const esmConfigs = {
defaultNextConfig: `/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
`
const nextConfigWithFunc = `const nextConfig = {
// Your Next.js config here
}
export default someFunc(nextConfig)
`
const nextConfigWithFuncMultiline = `const nextConfig = {
// Your Next.js config here
}
`,
nextConfigWithFunc: `const nextConfig = {};
export default someFunc(nextConfig);
`,
nextConfigWithFuncMultiline: `const nextConfig = {};;
export default someFunc(
nextConfig
)
`
const nextConfigExportNamedDefault = `const nextConfig = {
// Your Next.js config here
);
`,
nextConfigExportNamedDefault: `const nextConfig = {};
const wrapped = someFunc(asdf);
export { wrapped as default };
`,
nextConfigWithSpread: `const nextConfig = {
...someConfig,
};
export default nextConfig;
`,
}
const cjsConfigs = {
defaultNextConfig: `
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = nextConfig;
`,
anonConfig: `module.exports = {};`,
nextConfigWithFunc: `const nextConfig = {};
module.exports = someFunc(nextConfig);
`,
nextConfigWithFuncMultiline: `const nextConfig = {};
module.exports = someFunc(
nextConfig
);
`,
nextConfigExportNamedDefault: `const nextConfig = {};
const wrapped = someFunc(asdf);
module.exports = wrapped;
`,
nextConfigWithSpread: `const nextConfig = { ...someConfig };
module.exports = nextConfig;
`,
}
const wrapped = someFunc(asdf)
export { wrapped as default }
`
describe('parseAndInsertWithPayload', () => {
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(defaultNextConfig)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(nextConfigWithFunc)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
describe('esm', () => {
const configType = 'esm'
const importStatement = withPayloadStatement[configType]
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.defaultNextConfig,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.nextConfigWithFunc,
configType,
)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
})
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.nextConfigWithFuncMultiline,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
it('should parse the config with a spread', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.nextConfigWithSpread,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
// Unsupported: export { wrapped as default }
it('should give warning with a named export as default', () => {
const warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {})
const { modifiedConfigContent, success } = parseAndModifyConfigContent(
esmConfigs.nextConfigExportNamedDefault,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(success).toBe(false)
expect(warnLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Could not automatically wrap'),
)
})
})
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(nextConfigWithFuncMultiline)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
describe('cjs', () => {
const configType = 'cjs'
const requireStatement = withPayloadStatement[configType]
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.defaultNextConfig,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse anonymous default config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.anonConfig,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload({})')
})
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigWithFunc,
configType,
)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
})
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigWithFuncMultiline,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
it('should parse the config with a named export as default', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigExportNamedDefault,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload(wrapped)')
})
// Unsupported: export { wrapped as default }
it('should give warning with a named export as default', () => {
const warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {})
const { modifiedConfigContent, success } = parseAndModifyConfigContent(
nextConfigExportNamedDefault,
)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(success).toBe(false)
expect(warnLogSpy).toHaveBeenCalledWith(expect.stringContaining('Could not automatically wrap'))
it('should parse the config with a spread', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigWithSpread,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
})
})

View File

@@ -1,16 +1,29 @@
import type { Program } from 'esprima-next'
import chalk from 'chalk'
import { parseModule } from 'esprima'
import { Syntax, parseModule } from 'esprima-next'
import fs from 'fs'
import { warning } from '../utils/log.js'
import { log } from '../utils/log.js'
export const withPayloadImportStatement = `import { withPayload } from '@payloadcms/next'\n`
export const withPayloadStatement = {
cjs: `const { withPayload } = require('@payloadcms/next/withPayload')\n`,
esm: `import { withPayload } from '@payloadcms/next/withPayload'\n`,
}
export const wrapNextConfig = (args: { nextConfigPath: string }) => {
const { nextConfigPath } = args
type NextConfigType = 'cjs' | 'esm'
export const wrapNextConfig = (args: {
nextConfigPath: string
nextConfigType: NextConfigType
}) => {
const { nextConfigPath, nextConfigType: configType } = args
const configContent = fs.readFileSync(nextConfigPath, 'utf8')
const { modifiedConfigContent: newConfig, success } = parseAndModifyConfigContent(configContent)
const { modifiedConfigContent: newConfig, success } = parseAndModifyConfigContent(
configContent,
configType,
)
if (!success) {
return
@@ -22,72 +35,121 @@ export const wrapNextConfig = (args: { nextConfigPath: string }) => {
/**
* Parses config content with AST and wraps it with withPayload function
*/
export function parseAndModifyConfigContent(content: string): {
modifiedConfigContent: string
success: boolean
} {
content = withPayloadImportStatement + content
const ast = parseModule(content, { loc: true })
const exportDefaultDeclaration = ast.body.find((p) => p.type === 'ExportDefaultDeclaration') as
| Directive
| undefined
export function parseAndModifyConfigContent(
content: string,
configType: NextConfigType,
): { modifiedConfigContent: string; success: boolean } {
content = withPayloadStatement[configType] + content
const exportNamedDeclaration = ast.body.find((p) => p.type === 'ExportNamedDeclaration') as
| ExportNamedDeclaration
| undefined
if (!exportDefaultDeclaration && !exportNamedDeclaration) {
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
}
if (exportDefaultDeclaration && exportDefaultDeclaration.declaration?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
exportDefaultDeclaration.declaration.loc,
)
return { modifiedConfigContent, success: true }
} else if (exportNamedDeclaration) {
const exportSpecifier = exportNamedDeclaration.specifiers.find(
(s) =>
s.type === 'ExportSpecifier' &&
s.exported?.name === 'default' &&
s.local?.type === 'Identifier' &&
s.local?.name,
)
if (exportSpecifier) {
warning('Could not automatically wrap next.config.js with withPayload.')
warning('Automatic wrapping of named exports as default not supported yet.')
warnUserWrapNotSuccessful()
return {
modifiedConfigContent: content,
success: false,
}
let ast: Program | undefined
try {
ast = parseModule(content, { loc: true })
} catch (error: unknown) {
if (error instanceof Error) {
warning(`Unable to parse Next config. Error: ${error.message} `)
warnUserWrapNotSuccessful(configType)
}
return {
modifiedConfigContent: content,
success: false,
}
}
warning('Could not automatically wrap next.config.js with withPayload.')
warnUserWrapNotSuccessful()
if (configType === 'esm') {
const exportDefaultDeclaration = ast.body.find(
(p) => p.type === Syntax.ExportDefaultDeclaration,
) as Directive | undefined
const exportNamedDeclaration = ast.body.find(
(p) => p.type === Syntax.ExportNamedDeclaration,
) as ExportNamedDeclaration | undefined
if (!exportDefaultDeclaration && !exportNamedDeclaration) {
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
}
if (exportDefaultDeclaration && exportDefaultDeclaration.declaration?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
exportDefaultDeclaration.declaration.loc,
)
return { modifiedConfigContent, success: true }
} else if (exportNamedDeclaration) {
const exportSpecifier = exportNamedDeclaration.specifiers.find(
(s) =>
s.type === 'ExportSpecifier' &&
s.exported?.name === 'default' &&
s.local?.type === 'Identifier' &&
s.local?.name,
)
if (exportSpecifier) {
warning('Could not automatically wrap next.config.js with withPayload.')
warning('Automatic wrapping of named exports as default not supported yet.')
warnUserWrapNotSuccessful(configType)
return {
modifiedConfigContent: content,
success: false,
}
}
}
warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType)
return {
modifiedConfigContent: content,
success: false,
}
} else if (configType === 'cjs') {
// Find `module.exports = X`
const moduleExports = ast.body.find(
(p) =>
p.type === Syntax.ExpressionStatement &&
p.expression?.type === Syntax.AssignmentExpression &&
p.expression.left?.type === Syntax.MemberExpression &&
p.expression.left.object?.type === Syntax.Identifier &&
p.expression.left.object.name === 'module' &&
p.expression.left.property?.type === Syntax.Identifier &&
p.expression.left.property.name === 'exports',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any
if (moduleExports && moduleExports.expression.right?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
moduleExports.expression.right.loc,
)
return { modifiedConfigContent, success: true }
}
return {
modifiedConfigContent: content,
success: false,
}
}
warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType)
return {
modifiedConfigContent: content,
success: false,
}
}
function warnUserWrapNotSuccessful() {
function warnUserWrapNotSuccessful(configType: NextConfigType) {
// Output directions for user to update next.config.js
const withPayloadMessage = `
${chalk.bold(`Please manually wrap your existing next.config.js with the withPayload function. Here is an example:`)}
import withPayload from '@payloadcms/next/withPayload'
${withPayloadStatement[configType]}
const nextConfig = {
// Your Next.js config here
}
export default withPayload(nextConfig)
${configType === 'esm' ? 'export default withPayload(nextConfig)' : 'module.exports = withPayload(nextConfig)'}
`

View File

@@ -20,32 +20,41 @@ export async function writeEnvFile(args: {
return
}
const envOutputPath = path.join(projectDir, '.env')
try {
if (template?.type === 'starter' && fs.existsSync(path.join(projectDir, '.env.example'))) {
// Parse .env file into key/value pairs
const envFile = await fs.readFile(path.join(projectDir, '.env.example'), 'utf8')
const envWithValues: string[] = envFile
.split('\n')
.filter((e) => e)
.map((line) => {
if (line.startsWith('#') || !line.includes('=')) return line
if (fs.existsSync(envOutputPath)) {
if (template?.type === 'starter') {
// Parse .env file into key/value pairs
const envFile = await fs.readFile(path.join(projectDir, '.env.example'), 'utf8')
const envWithValues: string[] = envFile
.split('\n')
.filter((e) => e)
.map((line) => {
if (line.startsWith('#') || !line.includes('=')) return line
const split = line.split('=')
const key = split[0]
let value = split[1]
const split = line.split('=')
const key = split[0]
let value = split[1]
if (key === 'MONGODB_URI' || key === 'MONGO_URL' || key === 'DATABASE_URI') {
value = databaseUri
}
if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') {
value = payloadSecret
}
if (key === 'MONGODB_URI' || key === 'MONGO_URL' || key === 'DATABASE_URI') {
value = databaseUri
}
if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') {
value = payloadSecret
}
return `${key}=${value}`
})
return `${key}=${value}`
})
// Write new .env file
await fs.writeFile(path.join(projectDir, '.env'), envWithValues.join('\n'))
// Write new .env file
await fs.writeFile(envOutputPath, envWithValues.join('\n'))
} else {
const existingEnv = await fs.readFile(envOutputPath, 'utf8')
const newEnv =
existingEnv + `\nDATABASE_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}\n`
await fs.writeFile(envOutputPath, newEnv)
}
} else {
const content = `DATABASE_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}`
await fs.outputFile(`${projectDir}/.env`, content)

View File

@@ -21,6 +21,12 @@ export function helpMessage(): void {
console.log(chalk`
{bold USAGE}
{dim Inside of an existing Next.js project}
{dim $} {bold npx create-payload-app}
{dim Create a new project from scratch}
{dim $} {bold npx create-payload-app}
{dim $} {bold npx create-payload-app} my-project
{dim $} {bold npx create-payload-app} -n my-project -t template-name
@@ -80,7 +86,7 @@ export function successfulNextInit(): string {
}
export function moveMessage(args: { nextAppDir: string; projectDir: string }): string {
const relativePath = path.relative(process.cwd(), args.nextAppDir)
const relativeAppDir = path.relative(process.cwd(), args.nextAppDir)
return `
${header('Next Steps:')}
@@ -88,7 +94,10 @@ Payload does not support a top-level layout.tsx file in the app directory.
${chalk.bold('To continue:')}
Move all files from ./${relativePath} to a named directory such as ./${relativePath}/${chalk.bold('(app)')}
- Create a new directory in ./${relativeAppDir} such as ./${relativeAppDir}/${chalk.bold('(app)')}
- Move all files from ./${relativeAppDir} into that directory
It is recommended to do this from your IDE if your app has existing file references.
Once moved, rerun the create-payload-app command again.
`

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-alpha.59",
"version": "3.0.0-beta.11",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": {
"type": "git",

View File

@@ -28,6 +28,7 @@ export const init: Init = function init(this: MongooseAdapter) {
const versionSchema = buildSchema(this.payload.config, versionCollectionFields, {
disableUnique: true,
draftsEnabled: true,
indexSortableFields: this.payload.config.indexSortableFields,
options: {
minimize: false,
timestamps: false,
@@ -56,12 +57,6 @@ export const init: Init = function init(this: MongooseAdapter) {
this.autoPluralization === true ? undefined : collection.slug,
) as CollectionModel
this.collections[collection.slug] = model
// TS expect error only needed until we launch 2.0.0
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
this.payload.collections[collection.slug] = {
config: collection,
}
})
const model = buildGlobalModel(this.payload.config)

View File

@@ -59,17 +59,12 @@ export async function buildSearchParam({
let hasCustomID = false
if (sanitizedPath === '_id') {
const customIDfield = payload.collections[collectionSlug]?.config.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
const customIDFieldType = payload.collections[collectionSlug]?.customIDType
let idFieldType: 'number' | 'text' = 'text'
if (customIDfield) {
if (customIDfield?.type === 'text' || customIDfield?.type === 'number') {
idFieldType = customIDfield.type
}
if (customIDFieldType) {
idFieldType = customIDFieldType
hasCustomID = true
}
@@ -213,18 +208,11 @@ export async function buildSearchParam({
} else {
;(Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]).forEach(
(relationTo) => {
const isRelatedToCustomNumberID = payload.collections[
relationTo
]?.config?.fields.find((relatedField) => {
return (
fieldAffectsData(relatedField) &&
relatedField.name === 'id' &&
relatedField.type === 'number'
)
})
const isRelatedToCustomNumberID =
payload.collections[relationTo]?.customIDType === 'number'
if (isRelatedToCustomNumberID) {
if (isRelatedToCustomNumberID.type === 'number') hasNumberIDRelation = true
hasNumberIDRelation = true
}
},
)

View File

@@ -1,18 +1,20 @@
import { sanitizeConfig } from 'payload/config'
import { SanitizedConfig, sanitizeConfig } from 'payload/config'
import { Config } from 'payload/config'
import { getLocalizedSortProperty } from './getLocalizedSortProperty.js'
const config = {
const config = sanitizeConfig({
localization: {
locales: ['en', 'es'],
defaultLocale: 'en',
fallback: true,
},
} as Config
} as Config) as SanitizedConfig
describe('get localized sort property', () => {
it('passes through a non-localized sort property', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'title',
@@ -28,7 +30,7 @@ describe('get localized sort property', () => {
it('properly localizes an un-localized sort property', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'title',
@@ -45,7 +47,7 @@ describe('get localized sort property', () => {
it('keeps specifically asked-for localized sort properties', () => {
const result = getLocalizedSortProperty({
segments: ['title', 'es'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'title',
@@ -62,7 +64,7 @@ describe('get localized sort property', () => {
it('properly localizes nested sort properties', () => {
const result = getLocalizedSortProperty({
segments: ['group', 'title'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'group',
@@ -85,7 +87,7 @@ describe('get localized sort property', () => {
it('keeps requested locale with nested sort properties', () => {
const result = getLocalizedSortProperty({
segments: ['group', 'title', 'es'],
config: sanitizeConfig(config),
config,
fields: [
{
name: 'group',
@@ -108,7 +110,7 @@ describe('get localized sort property', () => {
it('properly localizes field within row', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config: sanitizeConfig(config),
config,
fields: [
{
type: 'row',
@@ -130,7 +132,7 @@ describe('get localized sort property', () => {
it('properly localizes field within named tab', () => {
const result = getLocalizedSortProperty({
segments: ['tab', 'title'],
config: sanitizeConfig(config),
config,
fields: [
{
type: 'tabs',
@@ -157,7 +159,7 @@ describe('get localized sort property', () => {
it('properly localizes field within unnamed tab', () => {
const result = getLocalizedSortProperty({
segments: ['title'],
config: sanitizeConfig(config),
config,
fields: [
{
type: 'tabs',

View File

@@ -142,7 +142,10 @@ export const sanitizeQueryValue = ({
if (path !== '_id' || (path === '_id' && hasCustomID && field.type === 'text')) {
if (operator === 'contains') {
formattedValue = { $options: 'i', $regex: formattedValue }
formattedValue = {
$options: 'i',
$regex: formattedValue.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'),
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-alpha.59",
"version": "3.0.0-beta.11",
"description": "The officially supported Postgres database adapter for Payload",
"repository": {
"type": "git",

View File

@@ -120,7 +120,7 @@ export const findMany = async function find({
const findPromise = db.query[tableName].findMany(findManyArgs)
if (pagination !== false && (orderedIDs ? orderedIDs?.length >= limit : true)) {
if (pagination !== false && (orderedIDs ? orderedIDs?.length <= limit : true)) {
const selectCountMethods: ChainedMethods = []
joinAliases.forEach(({ condition, table }) => {

View File

@@ -43,9 +43,11 @@ export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
export { sql } from 'drizzle-orm'
export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter> {
const postgresIDType = args.idType || 'serial'
const payloadIDType = postgresIDType ? 'number' : 'text'
function adapter({ payload }: { payload: Payload }) {
const migrationDir = findMigrationDir(args.migrationDir)
const idType = args.idType || 'serial'
return createDatabaseAdapter<PostgresAdapter>({
name: 'postgres',
@@ -55,7 +57,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
drizzle: undefined,
enums: {},
fieldConstraints: {},
idType,
idType: postgresIDType,
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger,
pgSchema: undefined,
@@ -79,10 +81,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
createGlobalVersion,
createMigration,
createVersion,
/**
* This represents how a default ID is treated in Payload as were a field type
*/
defaultIDType: idType === 'serial' ? 'number' : 'text',
defaultIDType: payloadIDType,
deleteMany,
deleteOne,
deleteVersions,
@@ -111,7 +110,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
}
return {
defaultIDType: 'number',
defaultIDType: payloadIDType,
init: adapter,
}
}

View File

@@ -225,6 +225,85 @@ export const getTableColumnFromPath = ({
})
}
case 'select': {
if (field.hasMany) {
newTableName = getTableName({
adapter,
config: field,
parentTableName: `${tableName}_${tableNameSuffix}`,
prefix: `${tableName}_${tableNameSuffix}`,
})
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
eq(adapter.tables[newTableName]._locale, locale),
)
if (locale !== 'all') {
constraints.push({
columnName: '_locale',
table: adapter.tables[newTableName],
value: locale,
})
}
} else {
joins[newTableName] = eq(
adapter.tables[tableName].id,
adapter.tables[newTableName].parent,
)
}
return {
columnName: 'value',
constraints,
field,
table: adapter.tables[newTableName],
}
}
break
}
case 'text':
case 'number': {
if (field.hasMany) {
let tableType = 'texts'
let columnName = 'text'
if (field.type === 'number') {
tableType = 'numbers'
columnName = 'number'
}
newTableName = `${tableName}_${tableType}`
const joinConstraints = [
eq(adapter.tables[tableName].id, adapter.tables[newTableName].parent),
eq(adapter.tables[newTableName].path, `${constraintPath}${field.name}`),
]
if (locale && field.localized && adapter.payload.config.localization) {
joins[newTableName] = and(
...joinConstraints,
eq(adapter.tables[newTableName]._locale, locale),
)
if (locale !== 'all') {
constraints.push({
columnName: 'locale',
table: adapter.tables[newTableName],
value: locale,
})
}
} else {
joins[newTableName] = and(...joinConstraints)
}
return {
columnName,
constraints,
field,
table: adapter.tables[newTableName],
}
}
break
}
case 'array': {
newTableName = getTableName({
adapter,
@@ -485,43 +564,41 @@ export const getTableColumnFromPath = ({
value,
})
}
}
default: {
if (fieldAffectsData(field)) {
if (field.localized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined
// so it is not picked up below to be used as targetTable
newTableName = `${tableName}${adapter.localesSuffix}`
if (fieldAffectsData(field)) {
if (field.localized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined
// so it is not picked up below to be used as targetTable
newTableName = `${tableName}${adapter.localesSuffix}`
const parentTable = aliasTable || adapter.tables[tableName]
const parentTable = aliasTable || adapter.tables[tableName]
joins[newTableName] = eq(parentTable.id, adapter.tables[newTableName]._parentID)
joins[newTableName] = eq(parentTable.id, adapter.tables[newTableName]._parentID)
aliasTable = undefined
aliasTable = undefined
if (locale !== 'all') {
constraints.push({
columnName: '_locale',
table: adapter.tables[newTableName],
value: locale,
})
}
}
const targetTable = aliasTable || adapter.tables[newTableName]
selectFields[`${newTableName}.${columnPrefix}${field.name}`] =
targetTable[`${columnPrefix}${field.name}`]
return {
columnName: `${columnPrefix}${field.name}`,
constraints,
field,
pathSegments,
table: targetTable,
}
if (locale !== 'all') {
constraints.push({
columnName: '_locale',
table: adapter.tables[newTableName],
value: locale,
})
}
}
const targetTable = aliasTable || adapter.tables[newTableName]
selectFields[`${newTableName}.${columnPrefix}${field.name}`] =
targetTable[`${columnPrefix}${field.name}`]
return {
columnName: `${columnPrefix}${field.name}`,
constraints,
field,
pathSegments,
table: targetTable,
}
}
}

View File

@@ -66,13 +66,6 @@ export const buildTable = ({
const columns: Record<string, PgColumnBuilder> = baseColumns
const indexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
let hasLocalizedField = false
let hasLocalizedRelationshipField = false
let hasManyTextField: 'index' | boolean = false
let hasManyNumberField: 'index' | boolean = false
let hasLocalizedManyTextField = false
let hasLocalizedManyNumberField = false
const localesColumns: Record<string, PgColumnBuilder> = {}
const localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
let localesTable: GenericTable | PgTableWithColumns<any>
@@ -89,7 +82,7 @@ export const buildTable = ({
const idColType: IDType = setColumnID({ adapter, columns, fields })
;({
const {
hasLocalizedField,
hasLocalizedManyNumberField,
hasLocalizedManyTextField,
@@ -116,7 +109,7 @@ export const buildTable = ({
rootTableIDColType: rootTableIDColType || idColType,
rootTableName,
versions,
}))
})
if (timestamps) {
columns.createdAt = timestamp('created_at', {
@@ -298,11 +291,12 @@ export const buildTable = ({
throwValidationError: true,
})
let colType = adapter.idType === 'uuid' ? 'uuid' : 'integer'
const relatedCollectionCustomID = relationshipConfig.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
if (relatedCollectionCustomID?.type === 'number') colType = 'numeric'
if (relatedCollectionCustomID?.type === 'text') colType = 'varchar'
const relatedCollectionCustomIDType =
adapter.payload.collections[relationshipConfig.slug]?.customIDType
if (relatedCollectionCustomIDType === 'number') colType = 'numeric'
if (relatedCollectionCustomIDType === 'text') colType = 'varchar'
relationshipColumns[`${relationTo}ID`] = parentIDColumnMap[colType](
`${formattedRelationTo}_id`,

View File

@@ -493,6 +493,7 @@ export const traverseFields = ({
localized: field.localized,
rootTableName,
table: adapter.tables[blockTableName],
tableLocales: adapter.tables[`${blockTableName}${adapter.localesSuffix}`],
})
}
adapter.blockTableNames[`${rootTableName}.${toSnakeCase(block.slug)}`] = blockTableName

View File

@@ -10,9 +10,13 @@ type Args = {
localized: boolean
rootTableName: string
table: GenericTable
tableLocales?: GenericTable
}
const getFlattenedFieldNames = (fields: Field[], prefix: string = ''): string[] => {
const getFlattenedFieldNames = (
fields: Field[],
prefix: string = '',
): { localized?: boolean; name: string }[] => {
return fields.reduce((fieldsToUse, field) => {
let fieldPrefix = prefix
@@ -44,7 +48,13 @@ const getFlattenedFieldNames = (fields: Field[], prefix: string = ''): string[]
}
if (fieldAffectsData(field)) {
return [...fieldsToUse, `${fieldPrefix?.replace('.', '_') || ''}${field.name}`]
return [
...fieldsToUse,
{
name: `${fieldPrefix?.replace('.', '_') || ''}${field.name}`,
localized: field.localized,
},
]
}
return fieldsToUse
@@ -56,22 +66,26 @@ export const validateExistingBlockIsIdentical = ({
localized,
rootTableName,
table,
tableLocales,
}: Args): void => {
const fieldNames = getFlattenedFieldNames(block.fields)
const missingField =
// ensure every field from the config is in the matching table
fieldNames.find((name) => Object.keys(table).indexOf(name) === -1) ||
fieldNames.find(({ name, localized }) => {
const fieldTable = localized && tableLocales ? tableLocales : table
return Object.keys(fieldTable).indexOf(name) === -1
}) ||
// ensure every table column is matched for every field from the config
Object.keys(table).find((fieldName) => {
if (!['_locale', '_order', '_parentID', '_path', '_uuid'].includes(fieldName)) {
return fieldNames.indexOf(fieldName) === -1
return fieldNames.findIndex((field) => field.name) === -1
}
})
if (missingField) {
throw new InvalidConfiguration(
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One block includes the field ${missingField}, while the other block does not.`,
`The table ${rootTableName} has multiple blocks with slug ${block.slug}, but the schemas do not match. One block includes the field ${typeof missingField === 'string' ? missingField : missingField.name}, while the other block does not.`,
)
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-alpha.59",
"version": "3.0.0-beta.11",
"main": "./src/index.ts",
"types": "./src/index.d.ts",
"type": "module",

View File

@@ -39,7 +39,13 @@
}
},
"publishConfig": {
"exports": null,
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"

View File

@@ -32,7 +32,13 @@
}
},
"publishConfig": {
"exports": null,
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.js",
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"

15
packages/next/.swcrc-cjs Normal file
View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": true,
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
}
},
"module": {
"type": "commonjs"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.0.0-alpha.59",
"version": "3.0.0-beta.11",
"main": "./src/index.js",
"types": "./src/index.js",
"type": "module",
@@ -11,7 +11,8 @@
"directory": "packages/next"
},
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && pnpm build:webpack && rm dist/prod/index.js",
"build:cjs": "swc ./src/withPayload.js -o ./dist/cjs/withPayload.cjs --config-file .swcrc-cjs",
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:cjs && pnpm build:types && pnpm build:webpack && rm dist/prod/index.js",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"build:webpack": "webpack --config webpack.config.js",
@@ -27,6 +28,10 @@
"require": "./src/index.js",
"types": "./src/index.js"
},
"./withPayload": {
"import": "./src/withPayload.js",
"require": "./src/withPayload.js"
},
"./*": {
"import": "./src/exports/*.ts",
"require": "./src/exports/*.ts",
@@ -72,7 +77,7 @@
},
"peerDependencies": {
"http-status": "1.6.2",
"next": "14.2.0-canary.23",
"next": "^14.2.0-canary.23",
"payload": "workspace:*"
},
"publishConfig": {
@@ -84,9 +89,9 @@
"require": "./dist/prod/styles.css",
"default": "./dist/prod/styles.css"
},
".": {
"import": "./dist/index.js",
"require": "./dist/index.js"
"./withPayload": {
"import": "./dist/withPayload.js",
"require": "./dist/cjs/withPayload.cjs"
},
"./*": {
"import": "./dist/exports/*.js",
@@ -97,7 +102,7 @@
"registry": "https://registry.npmjs.org/"
},
"engines": {
"node": ">=18.17.0"
"node": ">=18.20.2"
},
"files": [
"dist"

View File

@@ -1,2 +1,3 @@
export { getNextI18n } from '../utilities/getNextI18n.js'
export { traverseFields } from '../utilities/buildFieldSchemaMap/traverseFields.js'
export { getNextRequestI18n } from '../utilities/getNextRequestI18n.js'
export { getPayloadHMR } from '../utilities/getPayloadHMR.js'

View File

@@ -1,16 +1,18 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload/types'
import { translations } from '@payloadcms/translations/client'
import { rtlLanguages } from '@payloadcms/translations'
import { initI18n } from '@payloadcms/translations'
import { RootProvider } from '@payloadcms/ui/providers/Root'
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 { parseCookies } from 'payload/auth'
import { createClientConfig } from 'payload/config'
import { deepMerge } from 'payload/utilities'
import React from 'react'
import 'react-toastify/dist/ReactToastify.css'
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
import { DefaultEditView } from '../../views/Edit/Default/index.js'
import { DefaultListView } from '../../views/List/Default/index.js'
@@ -20,7 +22,15 @@ export const metadata = {
title: 'Next.js',
}
const rtlLanguages = ['ar', 'fa', 'ha', 'ku', 'ur', 'ps', 'dv', 'ks', 'khw', 'he', 'yi']
import { Merriweather } from 'next/font/google'
const merriweather = Merriweather({
display: 'swap',
style: ['normal', 'italic'],
subsets: ['latin'],
variable: '--font-serif',
weight: ['400', '900'],
})
export const RootLayout = async ({
children,
@@ -30,26 +40,37 @@ export const RootLayout = async ({
config: Promise<SanitizedConfig>
}) => {
const config = await configPromise
const clientConfig = await createClientConfig(config)
const headers = getHeaders()
const cookies = parseCookies(headers)
const lang =
getRequestLanguage({
config,
cookies,
headers,
}) ?? clientConfig.i18n.fallbackLanguage
const languageCode = getRequestLanguage({
config,
cookies,
headers,
})
const dir = rtlLanguages.includes(lang) ? 'RTL' : 'LTR'
const payload = await getPayloadHMR({ config })
const i18n = await initI18n({ config: config.i18n, context: 'client', language: languageCode })
const clientConfig = await createClientConfig({ config, t: i18n.t })
const mergedTranslations = deepMerge(translations, clientConfig.i18n.translations)
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
? 'RTL'
: 'LTR'
const languageOptions = Object.entries(translations || {}).map(([language, translations]) => ({
label: translations.general.thisLanguage,
value: language,
}))
const languageOptions = Object.entries(config.i18n.supportedLanguages || {}).reduce(
(acc, [language, languageConfig]) => {
if (Object.keys(config.i18n.supportedLanguages).includes(language)) {
acc.push({
label: languageConfig.translations.general.thisLanguage,
value: language,
})
}
return acc
},
[],
)
// eslint-disable-next-line @typescript-eslint/require-await
async function switchLanguageServerAction(lang: string): Promise<void> {
@@ -66,20 +87,23 @@ export const RootLayout = async ({
DefaultListView,
children,
config,
i18n,
payload,
})
return (
<html dir={dir} lang={lang}>
<html className={merriweather.variable} dir={dir} lang={languageCode}>
<body>
<RootProvider
componentMap={componentMap}
config={clientConfig}
dateFNSKey={i18n.dateFNSKey}
fallbackLang={clientConfig.i18n.fallbackLanguage}
lang={lang}
languageCode={languageCode}
languageOptions={languageOptions}
// eslint-disable-next-line react/jsx-no-bind
switchLanguageServerAction={switchLanguageServerAction}
translations={mergedTranslations[lang]}
translations={i18n.translations}
>
{wrappedChildren}
</RootProvider>

View File

@@ -77,7 +77,7 @@ export const tempFileHandler: Handler = (options, fieldname, filename) => {
}
export const memHandler: Handler = (options, fieldname, filename) => {
const buffers = []
const buffers: Buffer[] = []
const hash = crypto.createHash('md5')
let fileSize = 0
let completed = false

View File

@@ -1,4 +1,5 @@
const ACCEPTABLE_CONTENT_TYPE = /^multipart\/['"()+-_]+(?:; ?['"()+-_]*)+$/i
// eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/no-obscure-range
const ACCEPTABLE_CONTENT_TYPE = /multipart\/['"()+-_]+(?:; ?['"()+-_]*)+$/i
const UNACCEPTABLE_METHODS = new Set(['GET', 'HEAD', 'DELETE', 'OPTIONS', 'CONNECT', 'TRACE'])
const hasBody = (req: Request): boolean => {

View File

@@ -1,4 +1,5 @@
import Busboy from 'busboy'
import httpStatus from 'http-status'
import { APIError } from 'payload/errors'
import type { NextFileUploadOptions, NextFileUploadResponse } from './index.js'
@@ -17,6 +18,17 @@ type ProcessMultipart = (args: {
}) => Promise<NextFileUploadResponse>
export const processMultipart: ProcessMultipart = async ({ options, request }) => {
let parsingRequest = true
let fileCount = 0
let filesCompleted = 0
let allFilesHaveResolved: (value?: unknown) => void
let failedResolvingFiles: (err: Error) => void
const allFilesComplete = new Promise((res, rej) => {
allFilesHaveResolved = res
failedResolvingFiles = rej
})
const result: NextFileUploadResponse = {
fields: undefined,
files: undefined,
@@ -36,6 +48,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
// Build req.files fields
busboy.on('file', (field, file, info) => {
fileCount += 1
// Parse file name(cutting huge names, decoding, etc..).
const { encoding, filename: name, mimeType: mime } = info
const filename = parseFileName(options, name)
@@ -73,7 +86,9 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
debugLog(options, `Aborting upload because of size limit ${field}->${filename}.`)
cleanup()
parsingRequest = false
throw new APIError(options.responseOnLimit, 413, { size: getFileSize() })
throw new APIError(options.responseOnLimit, httpStatus.REQUEST_ENTITY_TOO_LARGE, {
size: getFileSize(),
})
}
})
@@ -95,6 +110,8 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
return debugLog(options, `Don't add file instance if original name and size are empty`)
}
filesCompleted += 1
result.files = buildFields(
result.files,
field,
@@ -117,19 +134,25 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
request[waitFlushProperty] = []
}
request[waitFlushProperty].push(writePromise)
if (filesCompleted === fileCount) {
allFilesHaveResolved()
}
})
file.on('error', (err) => {
uploadTimer.clear()
debugLog(options, `File Error: ${err.message}`)
cleanup()
failedResolvingFiles(err)
})
// Start upload process.
debugLog(options, `New upload started ${field}->${filename}, bytes:${getFileSize()}`)
uploadTimer.set()
})
busboy.on('finish', () => {
busboy.on('finish', async () => {
debugLog(options, `Busboy finished parsing request.`)
if (options.parseNested) {
result.fields = processNested(result.fields)
@@ -137,20 +160,27 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
}
if (request[waitFlushProperty]) {
Promise.all(request[waitFlushProperty]).then(() => {
delete request[waitFlushProperty]
})
try {
await Promise.all(request[waitFlushProperty]).then(() => {
delete request[waitFlushProperty]
})
} catch (err) {
debugLog(options, `Error waiting for file write promises: ${err}`)
}
}
return result
})
busboy.on('error', (err) => {
debugLog(options, `Busboy error`)
parsingRequest = false
throw new APIError('Busboy error parsing multipart request', 500)
throw new APIError('Busboy error parsing multipart request', httpStatus.BAD_REQUEST)
})
const reader = request.body.getReader()
// Start parsing request
while (parsingRequest) {
const { done, value } = await reader.read()
@@ -163,5 +193,7 @@ export const processMultipart: ProcessMultipart = async ({ options, request }) =
}
}
if (fileCount !== 0) await allFilesComplete
return result
}

View File

@@ -19,7 +19,7 @@ let tempCounter = 0
export const debugLog = (options: NextFileUploadOptions, msg: string) => {
const opts = options || {}
if (!opts.debug) return false
console.log(`Express-file-upload: ${msg}`) // eslint-disable-line
console.log(`Next-file-upload: ${msg}`) // eslint-disable-line
return true
}
@@ -287,8 +287,9 @@ export const parseFileName: ParseFileName = (opts, fileName) => {
? opts.safeFileNames
: SAFE_FILE_NAME_REGEX
// Parse file name extension.
let { name, extension } = parseFileNameExtension(opts.preserveExtension, parsedName)
if (extension.length) extension = '.' + extension.replace(nameRegex, '')
const parsedFileName = parseFileNameExtension(opts.preserveExtension, parsedName)
if (parsedFileName.extension.length)
parsedFileName.extension = '.' + parsedFileName.extension.replace(nameRegex, '')
return name.replace(nameRegex, '').concat(extension)
return parsedFileName.name.replace(nameRegex, '').concat(parsedFileName.extension)
}

View File

@@ -5,7 +5,7 @@ import { logoutOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'
export const logout: CollectionRouteHandler = async ({ collection, req }) => {
const result = logoutOperation({
const result = await logoutOperation({
collection,
req,
})

View File

@@ -1,5 +1,6 @@
import httpStatus from 'http-status'
import { generatePayloadCookie } from 'payload/auth'
import { ValidationError } from 'payload/errors'
import { registerFirstUserOperation } from 'payload/operations'
import type { CollectionRouteHandler } from '../types.js'
@@ -7,6 +8,15 @@ import type { CollectionRouteHandler } from '../types.js'
export const registerFirstUser: CollectionRouteHandler = async ({ collection, req }) => {
const data = req.data
if (data?.password !== data['confirm-password']) {
throw new ValidationError([
{
field: 'confirm-password',
message: req.t('Password and confirm password fields do not match.'),
},
])
}
const result = await registerFirstUserOperation({
collection,
data: {

View File

@@ -1,11 +1,5 @@
import type { BuildFormStateArgs } from '@payloadcms/ui/forms/buildStateFromSchema'
import type {
DocumentPreferences,
Field,
PayloadRequest,
SanitizedConfig,
TypeWithID,
} from 'payload/types'
import type { DocumentPreferences, Field, PayloadRequest, TypeWithID } from 'payload/types'
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
import { reduceFieldsToValues } from '@payloadcms/ui/utilities/reduceFieldsToValues'
@@ -22,12 +16,12 @@ if (!cached) {
cached = global._payload_fieldSchemaMap = null
}
export const getFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap => {
export const getFieldSchemaMap = (req: PayloadRequest): FieldSchemaMap => {
if (cached && process.env.NODE_ENV !== 'development') {
return cached
}
cached = buildFieldSchemaMap(config)
cached = buildFieldSchemaMap(req)
return cached
}
@@ -65,7 +59,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
})
}
const fieldSchemaMap = getFieldSchemaMap(req.payload.config)
const fieldSchemaMap = getFieldSchemaMap(req)
const id = collectionSlug ? reqData.id : undefined
const schemaPathSegments = schemaPath.split('.')
@@ -162,7 +156,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
})
}
if (globalSlug) {
if (globalSlug && schemaPath === globalSlug) {
resolvedData = await req.payload.findGlobal({
slug: globalSlug,
depth: 0,
@@ -193,6 +187,16 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
req,
})
// Maintain form state of file
if (
collectionSlug &&
req.payload.collections[collectionSlug]?.config?.upload &&
formState &&
formState.file
) {
result.file = formState.file
}
return Response.json(result, {
status: httpStatus.OK,
})

View File

@@ -4,9 +4,22 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
export const deleteByID: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const deleteByID: CollectionRouteHandlerWithID = async ({
id: incomingID,
collection,
req,
}) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const id = sanitizeCollectionID({
id: incomingID,
collectionSlug: collection.config.slug,
payload: req.payload,
})
const doc = await deleteByIDOperation({
id,
collection,

View File

@@ -5,12 +5,24 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
export const duplicate: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const duplicate: CollectionRouteHandlerWithID = async ({
id: incomingID,
collection,
req,
}) => {
const { searchParams } = req
const depth = searchParams.get('depth')
// draft defaults to true, unless explicitly set requested as false to prevent the newly duplicated document from being published
const draft = searchParams.get('draft') !== 'false'
const id = sanitizeCollectionID({
id: incomingID,
collectionSlug: collection.config.slug,
payload: req.payload,
})
const doc = await duplicateOperation({
id,
collection,

View File

@@ -4,10 +4,22 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
export const findByID: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const findByID: CollectionRouteHandlerWithID = async ({
id: incomingID,
collection,
req,
}) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const id = sanitizeCollectionID({
id: incomingID,
collectionSlug: collection.config.slug,
payload: req.payload,
})
const result = await findByIDOperation({
id,
collection,

View File

@@ -4,10 +4,22 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
export const findVersionByID: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const findVersionByID: CollectionRouteHandlerWithID = async ({
id: incomingID,
collection,
req,
}) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const id = sanitizeCollectionID({
id: incomingID,
collectionSlug: collection.config.slug,
payload: req.payload,
})
const result = await findVersionByIDOperation({
id,
collection,

View File

@@ -1,4 +1,5 @@
import httpStatus from 'http-status'
import { extractJWT } from 'payload/auth'
import { findByIDOperation } from 'payload/operations'
import { isNumber } from 'payload/utilities'
@@ -24,15 +25,19 @@ export const preview: CollectionRouteHandlerWithID = async ({ id, collection, re
(config) => config.slug === collection.config.slug,
)?.admin?.preview
const token = extractJWT(req)
if (typeof generatePreviewURL === 'function') {
try {
previewURL = await generatePreviewURL(result, {
locale: req.locale,
token: req.user?.token,
req,
token,
})
} catch (err) {
routeError({
return routeError({
collection,
config: req.payload.config,
err,
req,
})

View File

@@ -4,10 +4,22 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
export const restoreVersion: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const restoreVersion: CollectionRouteHandlerWithID = async ({
id: incomingID,
collection,
req,
}) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const id = sanitizeCollectionID({
id: incomingID,
collectionSlug: collection.config.slug,
payload: req.payload,
})
const result = await restoreVersionOperation({
id,
collection,

View File

@@ -4,12 +4,24 @@ import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
export const updateByID: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
import { sanitizeCollectionID } from '../utilities/sanitizeCollectionID.js'
export const updateByID: CollectionRouteHandlerWithID = async ({
id: incomingID,
collection,
req,
}) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const autosave = searchParams.get('autosave') === 'true'
const draft = searchParams.get('draft') === 'true'
const id = sanitizeCollectionID({
id: incomingID,
collectionSlug: collection.config.slug,
payload: req.payload,
})
const doc = await updateByIDOperation({
id,
autosave,

View File

@@ -66,6 +66,7 @@ export const getFile = async ({ collection, filename, req }: Args): Promise<Resp
} catch (error) {
return routeError({
collection,
config: req.payload.config,
err: error,
req,
})

View File

@@ -1,4 +1,5 @@
import httpStatus from 'http-status'
import { extractJWT } from 'payload/auth'
import { findOneOperation } from 'payload/operations'
import { isNumber } from 'payload/utilities'
@@ -24,14 +25,18 @@ export const preview: GlobalRouteHandler = async ({ globalConfig, req }) => {
(config) => config.slug === globalConfig.slug,
)?.admin?.preview
const token = extractJWT(req)
if (typeof generatePreviewURL === 'function') {
try {
previewURL = await generatePreviewURL(result, {
locale: req.locale,
token: req.user?.token,
req,
token,
})
} catch (err) {
routeError({
return routeError({
config: req.payload.config,
err,
req,
})

View File

@@ -303,6 +303,7 @@ export const GET =
} catch (error) {
return routeError({
collection,
config,
err: error,
req,
})
@@ -445,6 +446,7 @@ export const POST =
} catch (error) {
return routeError({
collection,
config,
err: error,
req,
})
@@ -514,6 +516,7 @@ export const DELETE =
} catch (error) {
return routeError({
collection,
config,
err: error,
req,
})
@@ -583,6 +586,7 @@ export const PATCH =
} catch (error) {
return routeError({
collection,
config,
err: error,
req,
})

View File

@@ -1,7 +1,9 @@
import type { Collection, PayloadRequest } from 'payload/types'
import type { Collection, PayloadRequest, SanitizedConfig } from 'payload/types'
import httpStatus from 'http-status'
import { APIError, ValidationError } from 'payload/errors'
import { APIError } from 'payload/errors'
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
export type ErrorResponse = { data?: any; errors: unknown[]; stack?: string }
@@ -66,26 +68,33 @@ const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorRes
}
}
export const routeError = ({
export const routeError = async ({
collection,
config: configArg,
err,
req,
}: {
collection?: Collection
config: Promise<SanitizedConfig> | SanitizedConfig
err: APIError
req: PayloadRequest
}) => {
if (!req?.payload) {
return Response.json(
{
message: err.message,
stack: err.stack,
},
{ status: httpStatus.INTERNAL_SERVER_ERROR },
)
let payload = req?.payload
if (!payload) {
try {
payload = await getPayloadHMR({ config: configArg })
} catch (e) {
return Response.json(
{
message: 'There was an error initializing Payload',
},
{ status: httpStatus.INTERNAL_SERVER_ERROR },
)
}
}
const { config, logger } = req.payload
const { config, logger } = payload
let response = formatErrors(err)

View File

@@ -0,0 +1,23 @@
import type { Payload } from 'payload/types'
type Args = {
collectionSlug: string
id: string
payload: Payload
}
export const sanitizeCollectionID = ({ id, collectionSlug, payload }: Args): number | string => {
let sanitizedID: number | string = id
const collection = payload.collections[collectionSlug]
// If default db ID type is a number, we should sanitize
let shouldSanitize = Boolean(payload.db.defaultIDType === 'number')
// UNLESS the customIDType for this collection is text.... then we leave it
if (shouldSanitize && collection.customIDType === 'text') shouldSanitize = false
// If we still should sanitize, parse float
if (shouldSanitize) sanitizedID = parseFloat(sanitizedID)
return sanitizedID
}

View File

@@ -1,4 +1,3 @@
@import './fonts.scss';
@import './styles.scss';
@import './toastify.scss';
@import './colors.scss';
@@ -20,9 +19,9 @@
--theme-overlay: rgba(5, 5, 5, 0.5);
--theme-baseline: #{$baseline-px};
--theme-baseline-body-size: #{$baseline-body-size};
--font-body: 'Suisse Intl', system-ui;
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif;
--font-mono: monospace;
--font-serif: 'Merriweather', serif;
--style-radius-s: #{$style-radius-s};
--style-radius-m: #{$style-radius-m};

View File

@@ -1,75 +0,0 @@
@font-face {
font-family: 'Suisse Intl';
src:
url('../assets/fonts/SuisseIntl.woff2') format('woff2'),
url('../assets/fonts/SuisseIntl.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Suisse Intl';
src:
url('../assets/fonts/SuisseIntl-Medium.woff2') format('woff2'),
url('../assets/fonts/SuisseIntl-Medium.woff') format('woff');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Suisse Intl';
src:
url('../assets/fonts/SuisseIntl-SemiBold.woff2') format('woff2'),
url('../assets/fonts/SuisseIntl-SemiBold.woff') format('woff');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'Suisse Intl';
src:
url('../assets/fonts/SuisseIntl-Bold.woff2') format('woff2'),
url('../assets/fonts/SuisseIntl-Bold.woff') format('woff');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'Merriweather';
font-style: normal;
font-weight: 400;
src:
local(''),
url('../assets/fonts/merriweather-v30-latin-regular.woff2') format('woff2'),
url('../assets/fonts/merriweather-v30-latin-regular.woff') format('woff');
}
@font-face {
font-family: 'Merriweather';
font-style: italic;
font-weight: 400;
src:
local(''),
url('../assets/fonts/merriweather-v30-latin-italic.woff2') format('woff2'),
url('../assets/fonts/merriweather-v30-latin-italic.woff') format('woff');
}
@font-face {
font-family: 'Merriweather';
font-style: normal;
font-weight: 900;
src:
local(''),
url('../assets/fonts/merriweather-v30-latin-900.woff2') format('woff2'),
url('../assets/fonts/merriweather-v30-latin-900.woff') format('woff');
}
@font-face {
font-family: 'Merriweather';
font-style: italic;
font-weight: 900;
src:
local(''),
url('../assets/fonts/merriweather-v30-latin-900italic.woff2') format('woff2'),
url('../assets/fonts/merriweather-v30-latin-900italic.woff') format('woff');
}

View File

@@ -21,13 +21,6 @@ $baseline: math.div($baseline-px, $baseline-body-size) + rem;
@return (math.div($baseline-px, $baseline-body-size) * $multiplier) + rem;
}
//////////////////////////////
// FONTS (DEPRECATED. DO NOT USE. PREFER CSS VARIABLES)
//////////////////////////////
$font-body: 'Suisse Intl' !default;
$font-mono: monospace !default;
//////////////////////////////
// COLORS (DEPRECATED. DO NOT USE. PREFER CSS VARIABLES)
//////////////////////////////

View File

@@ -1,10 +1,13 @@
import type { SanitizedConfig } from 'payload/types'
import type { PayloadRequest } from 'payload/types'
import type { FieldSchemaMap } from './types.js'
import { traverseFields } from './traverseFields.js'
export const buildFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap => {
export const buildFieldSchemaMap = ({
i18n,
payload: { config },
}: PayloadRequest): FieldSchemaMap => {
const result: FieldSchemaMap = new Map()
const validRelationships = config.collections.map((c) => c.slug) || []
@@ -13,6 +16,7 @@ export const buildFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap =>
traverseFields({
config,
fields: collection.fields,
i18n,
schemaMap: result,
schemaPath: collection.slug,
validRelationships,
@@ -23,6 +27,7 @@ export const buildFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap =>
traverseFields({
config,
fields: global.fields,
i18n,
schemaMap: result,
schemaPath: global.slug,
validRelationships,

View File

@@ -1,3 +1,4 @@
import type { I18n } from '@payloadcms/translations'
import type { Field, SanitizedConfig } from 'payload/types'
import { tabHasName } from 'payload/types'
@@ -7,6 +8,7 @@ import type { FieldSchemaMap } from './types.js'
type Args = {
config: SanitizedConfig
fields: Field[]
i18n: I18n
schemaMap: FieldSchemaMap
schemaPath: string
validRelationships: string[]
@@ -15,6 +17,7 @@ type Args = {
export const traverseFields = ({
config,
fields,
i18n,
schemaMap,
schemaPath,
validRelationships,
@@ -28,6 +31,7 @@ export const traverseFields = ({
traverseFields({
config,
fields: field.fields,
i18n,
schemaMap,
schemaPath: `${schemaPath}.${field.name}`,
validRelationships,
@@ -39,6 +43,7 @@ export const traverseFields = ({
traverseFields({
config,
fields: field.fields,
i18n,
schemaMap,
schemaPath,
validRelationships,
@@ -54,6 +59,7 @@ export const traverseFields = ({
traverseFields({
config,
fields: block.fields,
i18n,
schemaMap,
schemaPath: blockSchemaPath,
validRelationships,
@@ -65,6 +71,7 @@ export const traverseFields = ({
if (typeof field.editor.generateSchemaMap === 'function') {
field.editor.generateSchemaMap({
config,
i18n,
schemaMap,
schemaPath: `${schemaPath}.${field.name}`,
})
@@ -83,6 +90,7 @@ export const traverseFields = ({
traverseFields({
config,
fields: tab.fields,
i18n,
schemaMap,
schemaPath: tabSchemaPath,
validRelationships,

View File

@@ -6,7 +6,6 @@ import type {
} from 'payload/types'
import { initI18n } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
import { executeAuthStrategies } from 'payload/auth'
import { parseCookies } from 'payload/auth'
import { getDataLoader } from 'payload/utilities'
@@ -72,11 +71,10 @@ export const createPayloadRequest = async ({
headers: request.headers,
})
const i18n = initI18n({
const i18n = await initI18n({
config: config.i18n,
context: 'api',
language,
translations,
})
const customRequest: CustomPayloadRequest = {

View File

@@ -1,5 +1,7 @@
import type { Collection, CustomPayloadRequest, SanitizedConfig } from 'payload/types'
import type { NextFileUploadOptions } from '../next-fileupload/index.js'
import { nextFileUpload } from '../next-fileupload/index.js'
type GetDataAndFile = (args: {
@@ -10,64 +12,53 @@ type GetDataAndFile = (args: {
data: Record<string, any>
file: CustomPayloadRequest['file']
}>
export const getDataAndFile: GetDataAndFile = async ({ collection, config, request }) => {
export const getDataAndFile: GetDataAndFile = async ({
collection,
config,
request: incomingRequest,
}) => {
let data: Record<string, any> = undefined
let file: CustomPayloadRequest['file'] = undefined
if (['PATCH', 'POST', 'PUT'].includes(request.method.toUpperCase()) && request.body) {
if (
['PATCH', 'POST', 'PUT'].includes(incomingRequest.method.toUpperCase()) &&
incomingRequest.body
) {
const request = new Request(incomingRequest)
const [contentType] = (request.headers.get('Content-Type') || '').split(';')
if (contentType === 'application/json') {
data = await request.json()
} else if (contentType === 'multipart/form-data') {
// possible upload request
if (collection?.config?.upload) {
// load file in memory
if (!config.upload?.useTempFiles) {
const formData = await request.formData()
const formFile = formData.get('file')
if (formFile instanceof Blob) {
const bytes = await formFile.arrayBuffer()
const buffer = Buffer.from(bytes)
file = {
name: formFile.name,
data: buffer,
mimetype: formFile.type,
size: formFile.size,
}
}
const payloadData = formData.get('_payload')
if (typeof payloadData === 'string') {
data = JSON.parse(payloadData)
}
} else {
// store temp file on disk
const { error, fields, files } = await nextFileUpload({
options: config.upload as any,
request,
})
if (error) {
throw new Error(error.message)
}
if (files?.file) file = files.file
if (fields?._payload && typeof fields._payload === 'string') {
data = JSON.parse(fields._payload)
}
const bodyByteSize = parseInt(request.headers.get('Content-Length') || '0', 10)
const upperByteLimit =
typeof config.upload?.limits?.fieldSize === 'number'
? config.upload.limits.fields
: undefined
if (bodyByteSize <= upperByteLimit || upperByteLimit === undefined) {
try {
data = await request.json()
} catch (error) {
data = {}
}
} else {
// non upload request
const formData = await request.formData()
const payloadData = formData.get('_payload')
throw new Error('Request body size exceeds the limit')
}
} else {
if (request.headers.has('Content-Length') && request.headers.get('Content-Length') !== '0') {
const { error, fields, files } = await nextFileUpload({
options: config.upload as NextFileUploadOptions,
request,
})
if (typeof payloadData === 'string') {
data = JSON.parse(payloadData)
if (error) {
throw new Error(error.message)
}
if (collection?.config?.upload && files?.file) {
file = files.file
}
if (fields?._payload && typeof fields._payload === 'string') {
data = JSON.parse(fields._payload)
}
}
}

View File

@@ -1,22 +0,0 @@
import type { I18n } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload/types'
import { initI18n } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/client'
import { cookies, headers } from 'next/headers.js'
import { getRequestLanguage } from './getRequestLanguage.js'
export const getNextI18n = ({
config,
language,
}: {
config: SanitizedConfig
language?: string
}): I18n =>
initI18n({
config: config.i18n,
context: 'client',
language: language || getRequestLanguage({ config, cookies: cookies(), headers: headers() }),
translations,
})

View File

@@ -0,0 +1,19 @@
import type { I18n } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload/types'
import { initI18n } from '@payloadcms/translations'
import { cookies, headers } from 'next/headers.js'
import { getRequestLanguage } from './getRequestLanguage.js'
/**
* In the context of NextJS, this function initializes the i18n object for the current request.
*
* It must be called on the server side, and within the lifecycle of a request since it relies on the request headers and cookies.
*/
export const getNextRequestI18n = async ({ config }: { config: SanitizedConfig }): Promise<I18n> =>
initI18n({
config: config.i18n,
context: 'client',
language: getRequestLanguage({ config, cookies: cookies(), headers: headers() }),
})

View File

@@ -35,7 +35,10 @@ export const getPayloadHMR = async (options: InitOptions): Promise<Payload> => {
cached.payload.config = config
cached.payload.collections = config.collections.reduce((collections, collection) => {
collections[collection.slug] = { config: collection }
collections[collection.slug] = {
config: collection,
customIDType: cached.payload.collections[collection.slug]?.customIDType,
}
return collections
}, {})

View File

@@ -1,12 +1,13 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies.js'
import type { SanitizedConfig } from 'payload/config'
import { matchLanguage } from '@payloadcms/translations'
import { extractHeaderLanguage } from '@payloadcms/translations'
type GetRequestLanguageArgs = {
config: SanitizedConfig
cookies: Map<string, string> | ReadonlyRequestCookies
defaultLanguage?: string
defaultLanguage?: AcceptedLanguages
headers: Request['headers']
}
@@ -15,14 +16,23 @@ export const getRequestLanguage = ({
cookies,
defaultLanguage = 'en',
headers,
}: GetRequestLanguageArgs): string => {
const acceptLanguage = headers.get('Accept-Language')
const cookieLanguage = cookies.get(`${config.cookiePrefix || 'payload'}-lng`)
}: GetRequestLanguageArgs): AcceptedLanguages => {
const langCookie = cookies.get(`${config.cookiePrefix || 'payload'}-lng`)
const languageFromCookie = typeof langCookie === 'string' ? langCookie : langCookie?.value
const languageFromHeader = headers.get('Accept-Language')
? extractHeaderLanguage(headers.get('Accept-Language'))
: undefined
const fallbackLang = config?.i18n?.fallbackLanguage || defaultLanguage
const reqLanguage =
(typeof cookieLanguage === 'string' ? cookieLanguage : cookieLanguage?.value) ||
acceptLanguage ||
defaultLanguage
const supportedLanguageKeys = Object.keys(config?.i18n?.supportedLanguages || {})
return matchLanguage(reqLanguage)
if (languageFromCookie && supportedLanguageKeys.includes(languageFromCookie)) {
return languageFromCookie as AcceptedLanguages
}
if (languageFromHeader && supportedLanguageKeys.includes(languageFromHeader)) {
return languageFromHeader
}
return supportedLanguageKeys.includes(fallbackLang) ? (fallbackLang as AcceptedLanguages) : 'en'
}

View File

@@ -8,7 +8,6 @@ import type {
} from 'payload/types'
import { initI18n } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/client'
import { findLocaleFromCode } from '@payloadcms/ui/utilities/findLocaleFromCode'
import { headers as getHeaders } from 'next/headers.js'
import { notFound, redirect } from 'next/navigation.js'
@@ -26,6 +25,8 @@ type Args = {
searchParams: { [key: string]: string | string[] | undefined }
}
const authRoutes = ['/login', '/logout', '/create-first-user', '/forgot', '/reset', '/verify']
export const initPage = async ({
config: configPromise,
redirectUnauthenticatedUser = false,
@@ -45,14 +46,13 @@ export const initPage = async ({
const cookies = parseCookies(headers)
const language = getRequestLanguage({ config: payload.config, cookies, headers })
const i18n = initI18n({
const i18n = await initI18n({
config: payload.config.i18n,
context: 'client',
language,
translations,
})
const req = createLocalReq(
const req = await createLocalReq(
{
fallbackLocale: null,
locale: locale.code,
@@ -81,13 +81,19 @@ export const initPage = async ({
.filter(Boolean),
}
const routeSegments = route.replace(payload.config.routes.admin, '').split('/').filter(Boolean)
const {
routes: { admin: adminRoute },
} = payload.config
const routeSegments = route.replace(adminRoute, '').split('/').filter(Boolean)
const [entityType, entitySlug, createOrID] = routeSegments
const collectionSlug = entityType === 'collections' ? entitySlug : undefined
const globalSlug = entityType === 'globals' ? entitySlug : undefined
const docID = collectionSlug && createOrID !== 'create' ? createOrID : undefined
if (redirectUnauthenticatedUser && !user && route !== '/login') {
const isAuthRoute = authRoutes.some((r) => r === route.replace(adminRoute, ''))
if (redirectUnauthenticatedUser && !user && !isAuthRoute) {
if (searchParams && 'redirect' in searchParams) delete searchParams.redirect
const stringifiedSearchParams = Object.keys(searchParams ?? {}).length
@@ -97,6 +103,10 @@ export const initPage = async ({
redirect(`${routes.admin}/login?redirect=${route + stringifiedSearchParams}`)
}
if (!permissions.canAccessAdmin && !isAuthRoute) {
notFound()
}
let collectionConfig: SanitizedCollectionConfig
let globalConfig: SanitizedGlobalConfig

View File

@@ -18,7 +18,7 @@ import qs from 'qs'
import * as React from 'react'
import { toast } from 'react-toastify'
import { SetStepNav } from '../Edit/Default/SetStepNav/index.js'
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
import { RenderJSON } from './RenderJSON/index.js'
import './index.scss'
@@ -113,7 +113,7 @@ export const APIViewClient: React.FC = () => {
className={[baseClass, fullscreen && `${baseClass}--fullscreen`].filter(Boolean).join(' ')}
right={false}
>
<SetStepNav
<SetDocumentStepNav
collectionSlug={collectionSlug}
globalLabel={globalConfig?.label}
globalSlug={globalSlug}

View File

@@ -15,7 +15,7 @@ export const CreateFirstUserFields: React.FC<{
return (
<RenderFields
fieldMap={[...(fieldMap || []), ...(createFirstUserFieldMap || [])]}
fieldMap={[...(createFirstUserFieldMap || []), ...(fieldMap || [])]}
operation="create"
path=""
readOnly={false}

View File

@@ -1,9 +1,11 @@
import type { Field } from 'payload/types'
import type { FieldTypes } from 'payload/config'
import type { Field, WithServerSideProps as WithServerSidePropsType } from 'payload/types'
import type { AdminViewProps } from 'payload/types'
import { Form } from '@payloadcms/ui/forms/Form'
import { FormSubmit } from '@payloadcms/ui/forms/Submit'
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
import { WithServerSideProps as WithServerSidePropsGeneric } from '@payloadcms/ui/providers/ComponentMap'
import { mapFields } from '@payloadcms/ui/utilities/buildComponentMap'
import React from 'react'
@@ -16,6 +18,8 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
const {
req,
req: {
i18n,
payload,
payload: {
config,
config: {
@@ -48,10 +52,35 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
},
]
const WithServerSideProps: WithServerSidePropsType = ({ Component, ...rest }) => {
return <WithServerSidePropsGeneric Component={Component} payload={payload} {...rest} />
}
const createFirstUserFieldMap = mapFields({
WithServerSideProps,
config,
fieldSchema: fields,
i18n,
parentPath: userSlug,
}).map((field) => {
// Transform field types for the password and confirm-password fields
if (field.name === 'password') {
const type: keyof FieldTypes = 'password'
return {
...field,
type,
}
}
if (field.name === 'confirm-password') {
const type: keyof FieldTypes = 'confirmPassword'
return {
...field,
type,
}
}
return field
})
const formState = await buildStateFromSchema({

View File

@@ -3,7 +3,7 @@ import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload/t
import type { GenerateViewMetadata } from '../Root/index.js'
import { getNextI18n } from '../../utilities/getNextI18n.js'
import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
import { generateMetadata as apiMeta } from '../API/meta.js'
import { generateMetadata as editMeta } from '../Edit/meta.js'
import { generateMetadata as livePreviewMeta } from '../LivePreview/meta.js'
@@ -89,7 +89,7 @@ export const getMetaBySegment: GenerateEditViewMetadata = async ({
}
}
const i18n = await getNextI18n({
const i18n = await getNextRequestI18n({
config,
})

View File

@@ -7,10 +7,11 @@ import { useStepNav } from '@payloadcms/ui/elements/StepNav'
import { useConfig } from '@payloadcms/ui/providers/Config'
import { useDocumentInfo } from '@payloadcms/ui/providers/DocumentInfo'
import { useEditDepth } from '@payloadcms/ui/providers/EditDepth'
import { useEntityVisibility } from '@payloadcms/ui/providers/EntityVisibility'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import { useEffect } from 'react'
export const SetStepNav: React.FC<{
export const SetDocumentStepNav: React.FC<{
collectionSlug?: SanitizedCollectionConfig['slug']
globalLabel?: SanitizedGlobalConfig['label']
globalSlug?: SanitizedGlobalConfig['slug']
@@ -24,6 +25,8 @@ export const SetStepNav: React.FC<{
const view: string | undefined = props?.view || undefined
const { isEditing, title } = useDocumentInfo()
const { isEntityVisible } = useEntityVisibility()
const isVisible = isEntityVisible({ collectionSlug, globalSlug })
const { setStepNav } = useStepNav()
@@ -41,13 +44,13 @@ export const SetStepNav: React.FC<{
if (collectionSlug) {
nav.push({
label: getTranslation(pluralLabel, i18n),
url: `${admin}/collections/${collectionSlug}`,
url: isVisible ? `${admin}/collections/${collectionSlug}` : undefined,
})
if (isEditing) {
nav.push({
label: (useAsTitle && useAsTitle !== 'id' && title) || `${id}`,
url: `${admin}/collections/${collectionSlug}/${id}`,
url: isVisible ? `${admin}/collections/${collectionSlug}/${id}` : undefined,
})
} else {
nav.push({
@@ -57,7 +60,7 @@ export const SetStepNav: React.FC<{
} else if (globalSlug) {
nav.push({
label: title,
url: `${admin}/globals/${globalSlug}`,
url: isVisible ? `${admin}/globals/${globalSlug}` : undefined,
})
}
@@ -82,6 +85,7 @@ export const SetStepNav: React.FC<{
globalSlug,
view,
drawerDepth,
isVisible,
])
return null

View File

@@ -22,8 +22,8 @@ import React, { Fragment, useCallback } from 'react'
import { LeaveWithoutSaving } from '../../../elements/LeaveWithoutSaving/index.js'
// import { getTranslation } from '@payloadcms/translations'
import { Auth } from './Auth/index.js'
import { SetDocumentStepNav } from './SetDocumentStepNav/index.js'
import { SetDocumentTitle } from './SetDocumentTitle/index.js'
import { SetStepNav } from './SetStepNav/index.js'
import './index.scss'
const baseClass = 'collection-edit'
@@ -202,7 +202,7 @@ export const DefaultEditView: React.FC = () => {
/>
{BeforeDocument}
{preventLeaveWithoutSaving && <LeaveWithoutSaving />}
<SetStepNav
<SetDocumentStepNav
collectionSlug={collectionConfig?.slug}
globalSlug={globalConfig?.slug}
id={id}

View File

@@ -0,0 +1,81 @@
'use client'
import type { FormState, PayloadRequest } from 'payload/types'
import { Email } from '@payloadcms/ui/fields/Email'
import { Form } from '@payloadcms/ui/forms/Form'
import { FormSubmit } from '@payloadcms/ui/forms/Submit'
import { useConfig } from '@payloadcms/ui/providers/Config'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import { email } from 'payload/fields/validations'
import React, { Fragment, useState } from 'react'
import { toast } from 'react-toastify'
export const ForgotPasswordForm: React.FC = () => {
const config = useConfig()
const {
admin: { user: userSlug },
routes: { api },
} = config
const { t } = useTranslation()
const [hasSubmitted, setHasSubmitted] = useState(false)
const handleResponse = (res) => {
res.json().then(
() => {
setHasSubmitted(true)
},
() => {
toast.error(t('authentication:emailNotValid'))
},
)
}
const initialState: FormState = {
email: {
initialValue: '',
valid: true,
value: undefined,
},
}
if (hasSubmitted) {
return (
<Fragment>
<h1>{t('authentication:emailSent')}</h1>
<p>{t('authentication:checkYourEmailForPasswordReset')}</p>
</Fragment>
)
}
return (
<Form
action={`${api}/${userSlug}/forgot-password`}
handleResponse={handleResponse}
initialState={initialState}
method="POST"
>
<h1>{t('authentication:forgotPassword')}</h1>
<p>{t('authentication:forgotPasswordEmailInstructions')}</p>
<Email
autoComplete="email"
label={t('general:email')}
name="email"
required
validate={(value) =>
email(value, {
name: 'email',
type: 'email',
data: {},
req: { t } as PayloadRequest,
required: true,
siblingData: {},
})
}
/>
<FormSubmit>{t('general:submit')}</FormSubmit>
</Form>
)
}

View File

@@ -2,12 +2,11 @@ import type { AdminViewProps } from 'payload/types'
import { Button } from '@payloadcms/ui/elements/Button'
import { Translation } from '@payloadcms/ui/elements/Translation'
import { Email } from '@payloadcms/ui/fields/Email'
import { Form } from '@payloadcms/ui/forms/Form'
import { FormSubmit } from '@payloadcms/ui/forms/Submit'
import LinkImport from 'next/link.js'
import React, { Fragment } from 'react'
import { ForgotPasswordForm } from './ForgotPasswordForm/index.js'
export { generateForgotPasswordMetadata } from './meta.js'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
@@ -23,22 +22,9 @@ export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult })
} = initPageResult
const {
admin: { user: userSlug },
routes: { admin, api },
serverURL,
routes: { admin },
} = config
// const handleResponse = (res) => {
// res.json().then(
// () => {
// setHasSubmitted(true)
// },
// () => {
// toast.error(i18n.t('authentication:emailNotValid'))
// },
// )
// }
if (user) {
return (
<Fragment>
@@ -60,34 +46,9 @@ export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult })
)
}
// if (hasSubmitted) {
// return (
// <Fragment>
// <h1>{i18n.t('authentication:emailSent')}</h1>
// <p>{i18n.t('authentication:checkYourEmailForPasswordReset')}</p>
// </Fragment>
// )
// }
return (
<Fragment>
<Form
action={`${serverURL}${api}/${userSlug}/forgot-password`}
// handleResponse={handleResponse}
initialState={{
email: {
initialValue: '',
valid: false,
value: undefined,
},
}}
method="POST"
>
<h1>{i18n.t('authentication:forgotPassword')}</h1>
<p>{i18n.t('authentication:forgotPasswordEmailInstructions')}</p>
<Email autoComplete="email" label={i18n.t('general:emailAddress')} name="email" required />
<FormSubmit>{i18n.t('general:submit')}</FormSubmit>
</Form>
<ForgotPasswordForm />
<Link href={`${admin}/login`}>{i18n.t('authentication:backToLogin')}</Link>
</Fragment>
)

View File

@@ -78,7 +78,7 @@ export const ListView: React.FC<AdminViewProps> = async ({ initPageResult, searc
CustomListView = CustomList.Component
}
const page = isNumber(query?.page) ? query.page : 0
const page = isNumber(query?.page) ? Number(query.page) : 0
const whereQuery = mergeListSearchAndWhere({
collectionConfig,
query: {
@@ -87,7 +87,7 @@ export const ListView: React.FC<AdminViewProps> = async ({ initPageResult, searc
},
})
const limit = isNumber(query?.limit)
? query.limit
? Number(query.limit)
: listPreferences?.limit || collectionConfig.admin.pagination.defaultLimit
const sort =
query?.sort && typeof query.sort === 'string'
@@ -117,7 +117,10 @@ export const ListView: React.FC<AdminViewProps> = async ({ initPageResult, searc
<Fragment>
<HydrateClientUser permissions={permissions} user={user} />
<ListInfoProvider
collectionConfig={createClientCollectionConfig(collectionConfig)}
collectionConfig={createClientCollectionConfig({
collection: collectionConfig,
t: initPageResult.req.i18n.t,
})}
collectionSlug={collectionSlug}
hasCreatePermission={permissions?.collections?.[collectionSlug]?.create?.permission}
newDocumentURL={`${admin}/collections/${collectionSlug}/create`}

View File

@@ -18,8 +18,8 @@ import { getFormState } from '@payloadcms/ui/utilities/getFormState'
import React, { Fragment, useCallback } from 'react'
import { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving/index.js'
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
import { SetDocumentTitle } from '../Edit/Default/SetDocumentTitle/index.js'
import { SetStepNav } from '../Edit/Default/SetStepNav/index.js'
import { useLivePreviewContext } from './Context/context.js'
import { LivePreviewProvider } from './Context/index.js'
import { LivePreview } from './Preview/index.js'
@@ -140,7 +140,7 @@ const PreviewView: React.FC<Props> = ({
(globalConfig &&
!(globalConfig.versions?.drafts && globalConfig.versions?.drafts?.autosave))) &&
!disableLeaveWithoutSaving && <LeaveWithoutSaving />}
<SetStepNav
<SetDocumentStepNav
collectionSlug={collectionSlug}
globalLabel={globalConfig?.label}
globalSlug={globalSlug}

View File

@@ -6,7 +6,7 @@ import React from 'react'
const baseClass = 'login__form'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
import type { FormState } from 'payload/types'
import type { FormState, PayloadRequest } from 'payload/types'
import { FormLoadingOverlayToggle } from '@payloadcms/ui/elements/Loading'
import { Email } from '@payloadcms/ui/fields/Email'
@@ -15,6 +15,7 @@ import { Form } from '@payloadcms/ui/forms/Form'
import { FormSubmit } from '@payloadcms/ui/forms/Submit'
import { useConfig } from '@payloadcms/ui/providers/Config'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import { email, password } from 'payload/fields/validations'
import './index.scss'
@@ -57,8 +58,43 @@ export const LoginForm: React.FC<{
>
<FormLoadingOverlayToggle action="loading" name="login-form" />
<div className={`${baseClass}__inputWrap`}>
<Email autoComplete="email" label={t('general:email')} name="email" required />
<Password autoComplete="off" label={t('general:password')} name="password" required />
<Email
autoComplete="email"
label={t('general:email')}
name="email"
required
validate={(value) =>
email(value, {
name: 'email',
type: 'email',
data: {},
req: { t } as PayloadRequest,
required: true,
siblingData: {},
})
}
/>
<Password
autoComplete="off"
label={t('general:password')}
name="password"
required
validate={(value) =>
password(value, {
name: 'password',
type: 'text',
data: {},
req: {
payload: {
config,
},
t,
} as PayloadRequest,
required: true,
siblingData: {},
})
}
/>
</div>
<Link href={`${admin}/forgot`}>{t('authentication:forgotPasswordQuestion')}</Link>
<FormSubmit>{t('authentication:login')}</FormSubmit>

View File

@@ -6,8 +6,8 @@ import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser'
import { DefaultTemplate } from '@payloadcms/ui/templates/Default'
import React, { Fragment } from 'react'
import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
import { initPage } from '../../utilities/initPage.js'
import { getNextI18n } from '.././../utilities/getNextI18n.js'
import { NotFoundClient } from './index.client.js'
export const generatePageMetadata = async ({
@@ -19,7 +19,7 @@ export const generatePageMetadata = async ({
}): Promise<Metadata> => {
const config = await configPromise
const i18n = getNextI18n({
const i18n = await getNextRequestI18n({
config,
})

View File

@@ -1,7 +1,7 @@
import type { Metadata } from 'next'
import type { SanitizedConfig } from 'payload/types'
import { getNextI18n } from '../../utilities/getNextI18n.js'
import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
import { generateAccountMetadata } from '../Account/index.js'
import { generateCreateFirstUserMetadata } from '../CreateFirstUser/index.js'
import { generateDashboardMetadata } from '../Dashboard/index.js'
@@ -49,7 +49,7 @@ export const generatePageMetadata = async ({ config: configPromise, params }: Ar
const isGlobal = segmentOne === 'globals'
const isCollection = segmentOne === 'collections'
const i18n = getNextI18n({
const i18n = await getNextRequestI18n({
config,
})

View File

@@ -85,7 +85,9 @@ export const SetStepNav: React.FC<{
url: `${adminRoute}/collections/${collectionSlug}/${id}/versions`,
},
{
label: doc?.createdAt ? formatDate(doc.createdAt, dateFormat, i18n?.language) : '',
label: doc?.createdAt
? formatDate({ date: doc.createdAt, i18n, pattern: dateFormat })
: '',
},
]
}
@@ -101,7 +103,9 @@ export const SetStepNav: React.FC<{
url: `${adminRoute}/globals/${globalConfig.slug}/versions`,
},
{
label: doc?.createdAt ? formatDate(doc.createdAt, dateFormat, i18n?.language) : '',
label: doc?.createdAt
? formatDate({ date: doc.createdAt, i18n, pattern: dateFormat })
: '',
},
]
}

View File

@@ -61,7 +61,7 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
} = config
const formattedCreatedAt = doc?.createdAt
? formatDate(doc.createdAt, dateFormat, i18n.language)
? formatDate({ date: doc.createdAt, i18n, pattern: dateFormat })
: ''
const originalDocFetchURL = `${serverURL}${apiRoute}/${globalSlug ? 'globals/' : ''}${

View File

@@ -88,7 +88,7 @@ export const SelectComparison: React.FC<Props> = (props) => {
setOptions((existingOptions) => [
...existingOptions,
...data.docs.map((doc) => ({
label: formatDate(doc.updatedAt, dateFormat, i18n.language),
label: formatDate({ date: doc.updatedAt, i18n, pattern: dateFormat }),
value: doc.id,
})),
])

View File

@@ -22,7 +22,7 @@ export const generateMetadata: GenerateEditViewMetadata = async ({
const doc: any = {} // TODO: figure this out
const formattedCreatedAt = doc?.createdAt
? formatDate(doc.createdAt, config?.admin?.dateFormat, i18n?.language)
? formatDate({ date: doc.createdAt, i18n, pattern: config?.admin?.dateFormat })
: ''
if (collectionConfig) {

View File

@@ -38,7 +38,8 @@ export const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
return (
<Link href={to}>
{cellData && formatDate(cellData as Date | number | string, dateFormat, i18n.language)}
{cellData &&
formatDate({ date: cellData as Date | number | string, i18n, pattern: dateFormat })}
</Link>
)
}

View File

@@ -7,7 +7,7 @@ import { notFound } from 'next/navigation.js'
import { isNumber } from 'payload/utilities'
import React from 'react'
import { SetStepNav } from '../Edit/Default/SetStepNav/index.js'
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
import { buildVersionColumns } from './buildColumns.js'
import { VersionsViewClient } from './index.client.js'
import './index.scss'
@@ -100,8 +100,8 @@ export const VersionsView: EditViewComponent = async (props) => {
return (
<React.Fragment>
<SetStepNav
collectionSlug={collectionConfig?.slug || globalConfig?.slug}
<SetDocumentStepNav
collectionSlug={collectionConfig?.slug}
globalSlug={globalConfig?.slug}
id={id}
pluralLabel={collectionConfig?.labels?.plural || globalConfig?.label}

View File

@@ -1,5 +1,9 @@
/** @type {import('next').NextConfig} */
const withPayload = (nextConfig = {}) => {
/**
* @param {import('next').NextConfig} nextConfig
*
* @returns {import('next').NextConfig}
* */
export const withPayload = (nextConfig = {}) => {
return {
...nextConfig,
experimental: {

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"composite": true, // Required for references to work
"noEmit": false /* Do not emit outputs. */,
"emitDeclarationOnly": true,
"outDir": "./dist/cjs" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
"sourceMap": true
},
"include": ["src/withPayload.js" /* Include the withPayload.js file in the build */]
}

View File

@@ -1,26 +0,0 @@
/fields/
/components/
/auth.d.ts
/auth.js
/components.d.ts
/components.js
/config.d.ts
/config.js
/database.d.ts
/database.js
/errors.d.ts
/errors.js
/graphql.d.ts
/graphql.js
/types.d.ts
/types.js
/utilities.d.ts
/utilities.js
/versions.d.ts
/versions.js
/operations.js
/operations.d.ts
/node.js
/node.d.ts
/uploads.js
/uploads.d.ts

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.0.0-alpha.59",
"version": "3.0.0-beta.11",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./src/index.ts",
@@ -22,7 +22,7 @@
}
},
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types && tsx ../../scripts/exportPointerFiles.ts ../packages/payload dist/exports",
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"build:watch": "nodemon --watch 'src/**' --ext 'ts,tsx' --exec \"pnpm build:tsc\"",
@@ -52,6 +52,7 @@
"find-up": "4.1.0",
"get-tsconfig": "^4.7.2",
"http-status": "1.6.2",
"image-size": "^1.1.1",
"joi": "^17.12.1",
"json-schema-to-typescript": "11.0.3",
"jsonwebtoken": "9.0.1",
@@ -62,10 +63,18 @@
"pino": "8.15.0",
"pino-pretty": "10.2.0",
"pluralize": "8.0.0",
"probe-image-size": "^7.2.3",
"sanitize-filename": "1.6.3",
"scheduler": "0.23.0",
"scmp": "2.1.0"
"scmp": "2.1.0",
"uuid": "^9.0.1"
},
"peerDependencies": {
"@swc/core": "^1.4.13"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
}
},
"devDependencies": {
"@monaco-editor/react": "4.5.1",
@@ -113,7 +122,7 @@
"ts-essentials": "7.0.3"
},
"engines": {
"node": ">=18.17.0"
"node": ">=18.20.2"
},
"files": [
"bin.js",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
@import './dist/admin/scss/vars';
@import './dist/admin/scss/z-index';
@import './dist/admin/scss/type';
@import './dist/admin/scss/queries';
@import './dist/admin/scss/resets';
@import './dist/admin/scss/svg';

View File

@@ -1,8 +1,10 @@
import type { I18n } from '@payloadcms/translations'
import type { JSONSchema4 } from 'json-schema'
import type { SanitizedConfig } from '../config/types.js'
import type { Field, RichTextField, Validate } from '../fields/config/types.js'
import type { PayloadRequest, RequestContext } from '../types/index.js'
import type { WithServerSideProps } from './elements/WithServerSideProps.js'
export type RichTextFieldProps<
Value extends object,
@@ -27,11 +29,14 @@ type RichTextAdapterBase<
siblingDoc: Record<string, unknown>
}) => Promise<void> | null
generateComponentMap: (args: {
WithServerSideProps: WithServerSideProps
config: SanitizedConfig
i18n: I18n
schemaPath: string
}) => Map<string, React.ReactNode>
generateSchemaMap?: (args: {
config: SanitizedConfig
i18n: I18n
schemaMap: Map<string, Field[]>
schemaPath: string
}) => Map<string, Field[]>

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