Compare commits

..

11 Commits

Author SHA1 Message Date
Jessica Chowdhury
ea1af337f0 Merge branch 'main' into feat/live-preview-tab-default 2025-04-28 18:10:56 +01:00
Jessica Chowdhury
2cfce43dcf chore: revert unnecessary changes 2025-04-28 12:42:48 +01:00
Jessica Chowdhury
3bf3ed37b9 chore: revert accidental test change 2025-04-28 12:34:00 +01:00
Jessica Chowdhury
4ebbbb02f4 chore: fix metadata for default live preview and edit view 2025-04-28 11:01:45 +01:00
Jessica Chowdhury
dc911ceacf chore: merge conflict 2025-04-28 10:49:59 +01:00
Jessica Chowdhury
b0fa1e9768 Merge branch 'main' into feat/live-preview-tab-default 2025-04-17 12:35:54 +01:00
Jessica Chowdhury
fcc4b22715 docs: adds livePreview.defaultTab option to docs 2025-04-17 12:35:28 +01:00
Jessica Chowdhury
8abeca5568 chore: add e2e tests for livePreview.defaultTab 2025-04-17 12:19:42 +01:00
Jessica Chowdhury
68a3ae80f2 chore: abstracts live preview conditional logic 2025-04-17 11:19:40 +01:00
Jessica Chowdhury
d51c6d4f52 feat(next): adds option livePreview.defaultTab 2025-04-15 17:35:21 +01:00
Jessica Chowdhury
313bfff781 feat(next): updates default doc tab to live preview when enabled 2025-04-15 11:45:02 +01:00
1032 changed files with 10758 additions and 36800 deletions

View File

@@ -6,7 +6,7 @@ inputs:
node-version:
description: Node.js version
required: true
default: 23.11.0
default: 22.6.0
pnpm-version:
description: Pnpm version
required: true

View File

@@ -16,7 +16,7 @@ concurrency:
cancel-in-progress: true
env:
NODE_VERSION: 23.11.0
NODE_VERSION: 22.6.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
@@ -62,6 +62,12 @@ jobs:
echo "templates: ${{ steps.filter.outputs.templates }}"
lint:
# Follows same github's ci skip: [skip lint], [lint skip], [no lint]
if: >
github.event_name == 'pull_request' &&
!contains(github.event.pull_request.title, '[skip lint]') &&
!contains(github.event.pull_request.title, '[lint skip]') &&
!contains(github.event.pull_request.title, '[no lint]')
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
@@ -75,8 +81,10 @@ jobs:
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Lint
run: pnpm lint -- --quiet
- name: Lint staged
run: |
git diff --name-only --diff-filter=d origin/${GITHUB_BASE_REF}...${GITHUB_SHA}
npx lint-staged --diff="origin/${GITHUB_BASE_REF}...${GITHUB_SHA}"
build:
needs: changes

View File

@@ -7,7 +7,7 @@ on:
workflow_dispatch:
env:
NODE_VERSION: 23.11.0
NODE_VERSION: 22.6.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry

View File

@@ -12,7 +12,7 @@ on:
default: ''
env:
NODE_VERSION: 23.11.0
NODE_VERSION: 22.6.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry

View File

@@ -7,7 +7,7 @@ on:
workflow_dispatch:
env:
NODE_VERSION: 23.11.0
NODE_VERSION: 22.6.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry

1
.gitignore vendored
View File

@@ -3,7 +3,6 @@ package-lock.json
dist
/.idea/*
!/.idea/runConfigurations
/.idea/runConfigurations/_template*
!/.idea/payload.iml
# Custom actions

View File

@@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="true" type="JavaScriptTestRunnerJest">
<node-interpreter value="project" />
<node-options value="--no-deprecation" />
<envs />
<scope-kind value="ALL" />
<method v="2" />
</configuration>
</component>

View File

@@ -1 +1 @@
v23.11.0
v22.6.0

2
.nvmrc
View File

@@ -1 +1 @@
v23.11.0
v22.6.0

View File

@@ -1,2 +1,2 @@
pnpm 9.7.1
nodejs 23.11.0
nodejs 22.6.0

14
.vscode/launch.json vendored
View File

@@ -63,13 +63,6 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts query-presets",
"cwd": "${workspaceFolder}",
"name": "Run Dev Query Presets",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts login-with-username",
"cwd": "${workspaceFolder}",
@@ -118,13 +111,6 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts folder-view",
"cwd": "${workspaceFolder}",
"name": "Run Dev Folder View",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts localization",
"cwd": "${workspaceFolder}",

View File

@@ -7,6 +7,9 @@
},
"editor.formatOnSaveMode": "file",
"eslint.rules.customizations": [
// Defaultt all ESLint errors to 'warn' to differentate from TypeScript's 'error' level
{ "rule": "*", "severity": "warn" },
// Silence some warnings that will get auto-fixed
{ "rule": "perfectionist/*", "severity": "off", "fixable": true },
{ "rule": "curly", "severity": "off", "fixable": true },
@@ -21,8 +24,5 @@
"runtimeArgs": ["--no-deprecation"]
},
// Essentially disables bun test buttons
"bun.test.filePattern": "bun.test.ts",
"playwright.env": {
"NODE_OPTIONS": "--no-deprecation --no-experimental-strip-types"
}
"bun.test.filePattern": "bun.test.ts"
}

View File

@@ -39,7 +39,7 @@ const GlobalWithAccessControl: GlobalConfig = {
update: ({ req: { user } }) => {...},
// Version-enabled Globals only
readVersions: () => {...},
readVersion: () => {...},
},
// highlight-end
}
@@ -64,7 +64,7 @@ If a Global supports [Versions](../versions/overview), the following additional
Returns a boolean result or optionally a [query constraint](../queries/overview) which limits who can read this global based on its current properties.
To add read Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../configuration/globals):
To add read Access Control to a [Global](../configuration/globals), use the `read` property in the [Global Config](../configuration/globals):
```ts
import { GlobalConfig } from 'payload'
@@ -72,7 +72,7 @@ import { GlobalConfig } from 'payload'
const Header: GlobalConfig = {
// ...
// highlight-start
access: {
read: {
read: ({ req: { user } }) => {
return Boolean(user)
},

View File

@@ -65,7 +65,7 @@ preview: (doc, { req }) => `${req.protocol}//${req.host}/${doc.slug}` // highlig
The Preview feature can be used to achieve "Draft Preview". After clicking the preview button from the Admin Panel, you can enter into "draft mode" within your front-end application. This will allow you to adjust your page queries to include the `draft: true` param. When this param is present on the request, Payload will send back a draft document as opposed to a published one based on the document's `_status` field.
To enter draft mode, the URL provided to the `preview` function can point to a custom endpoint in your front-end application that sets a cookie or session variable to indicate that draft mode is enabled. This is framework specific, so the mechanisms here vary from framework to framework although the underlying concept is the same.
To enter draft mode, the URL provided to the `preview` function can point to a custom endpoint in your front-end application that sets a cookie or session variable to indicate that draft mode is enabled. This is framework specific, so the mechanisms here very from framework to framework although the underlying concept is the same.
### Next.js

View File

@@ -605,7 +605,7 @@ return (
textField: {
initialValue: 'Updated text',
valid: true,
value: 'Updated text',
value: 'Upddated text',
},
},
// blockType: "yourBlockSlug",
@@ -875,7 +875,7 @@ Useful to retrieve info about the currently logged in user as well as methods fo
| **`refreshCookie`** | A method to trigger the silent refreshing of a user's auth token |
| **`setToken`** | Set the token of the user, to be decoded and used to reset the user and token in memory |
| **`token`** | The logged in user's token (useful for creating preview links, etc.) |
| **`refreshPermissions`** | Load new permissions (useful when content that affects permissions has been changed) |
| **`refreshPermissions`** | Load new permissions (useful when content that effects permissions has been changed) |
| **`permissions`** | The permissions of the current user |
```tsx
@@ -1143,7 +1143,7 @@ This is useful for scenarios where you need to trigger another fetch regardless
Route transitions are useful in showing immediate visual feedback to the user when navigating between pages. This is especially useful on slow networks when navigating to data heavy or process intensive pages.
By default, any instances of `Link` from `@payloadcms/ui` will trigger route transitions by default.
By default, any instances of `Link` from `@payloadcms/ui` will trigger route transitions dy default.
```tsx
import { Link } from '@payloadcms/ui'

View File

@@ -62,7 +62,7 @@ In this scenario, if your cookie was still valid, malicious-intent.com would be
### CSRF Prevention
Define domains that you trust and are willing to accept Payload HTTP-only cookie based requests from. Use the `csrf` option on the base Payload Config to do this:
Define domains that your trust and are willing to accept Payload HTTP-only cookie based requests from. Use the `csrf` option on the base Payload Config to do this:
```ts
// payload.config.ts
@@ -102,8 +102,8 @@ If option 1 isn't possible, then you can get around this limitation by [configur
```
SameSite: None // allows the cookie to cross domains
Secure: true // ensures it's sent over HTTPS only
HttpOnly: true // ensures it's not accessible via client side JavaScript
Secure: true // ensures its sent over HTTPS only
HttpOnly: true // ensures its not accessible via client side JavaScript
```
Configuration example:

View File

@@ -71,7 +71,7 @@ export const Customers: CollectionConfig = {
#### generateEmailSubject
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function arguments are the same but you can only return a string - not HTML.
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function argument are the same but you can only return a string - not HTML.
```ts
import type { CollectionConfig } from 'payload'
@@ -178,7 +178,7 @@ The following arguments are passed to the `generateEmailHTML` function:
#### generateEmailSubject
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function arguments are the same but you can only return a string - not HTML.
Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function argument are the same but you can only return a string - not HTML.
```ts
import type { CollectionConfig } from 'payload'

View File

@@ -38,7 +38,7 @@ const request = await fetch('http://localhost:3000', {
### Omitting The Token
In some cases you may want to prevent the token from being returned from the auth operations. You can do that by setting `removeTokenFromResponses` to `true` like so:
In some cases you may want to prevent the token from being returned from the auth operations. You can do that by setting `removeTokenFromResponse` to `true` like so:
```ts
import type { CollectionConfig } from 'payload'
@@ -46,7 +46,7 @@ import type { CollectionConfig } from 'payload'
export const UsersWithoutJWTs: CollectionConfig = {
slug: 'users-without-jwts',
auth: {
removeTokenFromResponses: true, // highlight-line
removeTokenFromResponse: true, // highlight-line
},
}
```

View File

@@ -67,7 +67,7 @@ query {
}
```
Document access can also be queried on a collection/global basis. Access on a global can be queried like `http://localhost:3000/api/global-slug/access`, Collection document access can be queried like `http://localhost:3000/api/collection-slug/access/:id`.
Document access can also be queried on a collection/global basis. Access on a global can queried like `http://localhost:3000/api/global-slug/access`, Collection document access can be queried like `http://localhost:3000/api/collection-slug/access/:id`.
## Me

View File

@@ -71,7 +71,7 @@ export const Admins: CollectionConfig = {
</Banner>
<Banner type="warning">
**Note:** Auth-enabled Collections will be automatically injected with the
**Note:** Auth-enabled Collections with be automatically injected with the
`hash`, `salt`, and `email` fields. [More
details](../fields/overview#field-names).
</Banner>

View File

@@ -8,7 +8,7 @@ keywords: authentication, config, configuration, documentation, Content Manageme
During the lifecycle of a request you will be able to access the data you have configured to be stored in the JWT by accessing `req.user`. The user object is automatically appended to the request for you.
### Defining Token Data
### Definining Token Data
You can specify what data gets encoded to the Cookie/JWT-Token by setting `saveToJWT` property on fields within your auth collection.

View File

@@ -132,7 +132,6 @@ The following options are available:
| `hideAPIURL` | Hides the "API URL" meta field while editing documents within this Collection. |
| `enableRichTextLink` | The [Rich Text](../fields/rich-text) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| `enableRichTextRelationship` | The [Rich Text](../fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
| `folders` | A boolean to enable folders for a given collection. Defaults to `false`. [More details](../folders/overview). |
| `meta` | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](../admin/metadata). |
| `preview` | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](../admin/preview). |
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
@@ -177,7 +176,7 @@ The following options are available:
#### Edit View Options
```ts
import type { CollectionConfig } from 'payload'
import type { CollectionCOnfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
@@ -275,7 +274,7 @@ You can also pass an object to the collection's `graphQL` property, which allows
## TypeScript
You can import types from Payload to help make writing your Collection configs easier and type-safe. There are two main types that represent the Collection Config, `CollectionConfig` and `SanitizedCollectionConfig`.
You can import types from Payload to help make writing your Collection configs easier and type-safe. There are two main types that represent the Collection Config, `CollectionConfig` and `SanitizeCollectionConfig`.
The `CollectionConfig` type represents a raw Collection Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedCollectionConfig` type represents a Collection Config after it has been fully sanitized. Generally, this is only used internally by Payload.

View File

@@ -205,7 +205,7 @@ You can also pass an object to the global's `graphQL` property, which allows you
## TypeScript
You can import types from Payload to help make writing your Global configs easier and type-safe. There are two main types that represent the Global Config, `GlobalConfig` and `SanitizedGlobalConfig`.
You can import types from Payload to help make writing your Global configs easier and type-safe. There are two main types that represent the Global Config, `GlobalConfig` and `SanitizeGlobalConfig`.
The `GlobalConfig` type represents a raw Global Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedGlobalConfig` type represents a Global Config after it has been fully sanitized. Generally, this is only used internally by Payload.

View File

@@ -84,7 +84,6 @@ The following options are available:
| **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](../authentication/cookies#csrf-attacks). |
| **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). |
| **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. |
| `folders` | An optional object to configure global folder settings. [More details](../folders/overview). |
| `queryPresets` | An object that to configure Collection Query Presets. [More details](../query-presets/overview). |
| **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). |
| **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. |
@@ -213,7 +212,7 @@ For more information about what we track, take a look at our [privacy policy](/p
## Cross-origin resource sharing (CORS)#cors
Cross-origin resource sharing (CORS) can be configured with either a whitelist array of URLS to allow CORS requests from, a wildcard string (`*`) to accept incoming requests from any domain, or an object with the following properties:
Cross-origin resource sharing (CORS) can be configured with either a whitelist array of URLS to allow CORS requests from, a wildcard string (`*`) to accept incoming requests from any domain, or a object with the following properties:
| Option | Description |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
@@ -292,7 +291,7 @@ export const script = async (config: SanitizedConfig) => {
collection: 'pages',
data: { title: 'my title' },
})
payload.logger.info('Successfully seeded!')
payload.logger.info('Succesffully seeded!')
process.exit(0)
}
```

View File

@@ -59,7 +59,7 @@ _For details on how to build Custom Views, including all available props, see [B
### Document Root
The Document Root is mounted on the top-level route for a Document. Setting this property will completely take over the entire Document View layout, including the title, [Document Tabs](#document-tabs), _and all other nested Document Views_ including the [Edit View](./edit-view), API View, etc.
The Document Root is mounted on the top-level route for a Document. Setting this property will completely take over the entire Document View layout, including the title, [Document Tabs](#ocument-tabs), _and all other nested Document Views_ including the [Edit View](./edit-view), API View, etc.
When setting a Document Root, you are responsible for rendering all necessary components and controls, as no document controls or tabs would be rendered. To replace only the Edit View precisely, use the `edit.default` key instead.

View File

@@ -40,7 +40,7 @@ The following options are available:
| `beforeDashboard` | An array of Custom Components to inject into the built-in Dashboard, _before_ the default dashboard contents. [More details](#beforedashboard). |
| `beforeLogin` | An array of Custom Components to inject into the built-in Login, _before_ the default login form. [More details](#beforelogin). |
| `beforeNavLinks` | An array of Custom Components to inject into the built-in Nav, _before_ the links themselves. [More details](#beforenavlinks). |
| `graphics.Icon` | The simplified logo used in contexts like the `Nav` component. [More details](#graphicsicon). |
| `graphics.Icon` | The simplified logo used in contexts like the the `Nav` component. [More details](#graphicsicon). |
| `graphics.Logo` | The full logo used in contexts like the `Login` view. [More details](#graphicslogo). |
| `header` | An array of Custom Components to be injected above the Payload header. [More details](#header). |
| `logout.Button` | The button displayed in the sidebar that logs the user out. [More details](#logoutbutton). |
@@ -345,7 +345,7 @@ export default function MyCustomIcon() {
The `Logo` property is the full logo used in contexts like the `Login` view. This is typically a larger, more detailed representation of your brand.
To add a custom logo, use the `admin.components.graphics.Logo` property in your Payload Config:
To add a custom logo, use the `admin.components.graphic.Logo` property in your Payload Config:
```ts
import { buildConfig } from 'payload'

View File

@@ -298,15 +298,3 @@ Passing your migrations as shown above will tell Payload, in production only, to
may slow down serverless cold starts on platforms such as Vercel. Generally,
this option should only be used for long-running servers / containers.
</Banner>
## Environment-Specific Configurations and Migrations
Your configuration may include environment-specific settings (e.g., enabling a plugin only in production). If you generate migrations without considering the environment, it can lead to discrepancies and issues. When running migrations locally, Payload uses the development environment, which might miss production-specific configurations. Similarly, running migrations in production could miss development-specific entities.
This is an easy oversight, so be mindful of any environment-specific logic in your config when handling migrations.
**Ways to address this:**
- Manually update your migration file after it is generated to include any environment-specific configurations.
- Temporarily enable any required production environment variables in your local setup when generating the migration to capture the necessary updates.
- Use separate migration files for each environment to ensure the correct migration is executed in the corresponding environment.

View File

@@ -39,7 +39,7 @@ export default buildConfig({
import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres'
export default buildConfig({
// Automatically uses process.env.POSTGRES_URL if no options are provided.
// Automatically uses proces.env.POSTGRES_URL if no options are provided.
db: vercelPostgresAdapter(),
// Optionally, can accept the same options as the @vercel/postgres package.
db: vercelPostgresAdapter({
@@ -224,7 +224,7 @@ Make sure Payload doesn't overlap table names with its collections. For example,
### afterSchemaInit
Runs after the Drizzle schema is built. You can use this hook to modify the schema with features that aren't supported by Payload, or if you want to add a column that you don't want to be in the Payload config.
To extend a table, Payload exposes `extendTable` utility to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
To extend a table, Payload exposes `extendTable` utillity to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
The following example adds the `extra_integer_column` column and a composite index on `country` and `city` columns.
```ts

View File

@@ -189,7 +189,7 @@ Make sure Payload doesn't overlap table names with its collections. For example,
### afterSchemaInit
Runs after the Drizzle schema is built. You can use this hook to modify the schema with features that aren't supported by Payload, or if you want to add a column that you don't want to be in the Payload config.
To extend a table, Payload exposes `extendTable` utility to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
To extend a table, Payload exposes `extendTable` utillity to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
The following example adds the `extra_integer_column` column and a composite index on `country` and `city` columns.
```ts

View File

@@ -80,7 +80,7 @@ export const MyArrayField: Field = {
}
```
The Array Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Array Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ------------------------- | ----------------------------------------------------------------------------------- |

View File

@@ -78,7 +78,7 @@ export const MyBlocksField: Field = {
}
```
The Blocks Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Blocks Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ---------------------- | -------------------------------------------------------------------------- |

View File

@@ -68,7 +68,7 @@ export const MyCodeField: Field = {
}
```
The Code Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Code Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -58,7 +58,7 @@ export const MyCollapsibleField: Field = {
}
```
The Collapsible Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Collapsible Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ------------------- | ------------------------------- |

View File

@@ -65,7 +65,7 @@ export const MyDateField: Field = {
}
```
The Date Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Date Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Property | Description |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -65,7 +65,7 @@ export const MyEmailField: Field = {
}
```
The Email Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Email Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Property | Description |
| ------------------ | ------------------------------------------------------------------------- |

View File

@@ -35,9 +35,9 @@ export const MyGroupField: Field = {
| Option | Description |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`fields`** \* | Array of field types to nest within this Group. |
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. Required when name is undefined, defaults to name converted to words. |
| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
@@ -69,7 +69,7 @@ export const MyGroupField: Field = {
}
```
The Group Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Group Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -86,7 +86,7 @@ export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
name: 'pageMeta',
name: 'pageMeta', // required
type: 'group', // required
interfaceName: 'Meta', // optional
fields: [
@@ -110,38 +110,3 @@ export const ExampleCollection: CollectionConfig = {
],
}
```
## Presentational group fields
You can also use the Group field to create a presentational group of fields. This is useful when you want to group fields together visually without affecting the data structure.
The label will be required when a `name` is not provided.
```ts
import type { CollectionConfig } from 'payload'
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
label: 'Page meta',
type: 'group', // required
fields: [
{
name: 'title',
type: 'text',
required: true,
minLength: 20,
maxLength: 100,
},
{
name: 'description',
type: 'textarea',
required: true,
minLength: 40,
maxLength: 160,
},
],
},
],
}
```

View File

@@ -67,7 +67,7 @@ export const MyJSONField: Field = {
}
```
The JSON Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The JSON Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -70,7 +70,7 @@ export const MyNumberField: Field = {
}
```
The Number Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Number Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Property | Description |
| ------------------ | --------------------------------------------------------------------------------- |

View File

@@ -100,7 +100,7 @@ Here are the available Presentational Fields:
### Virtual Fields
Virtual fields are used to display data that is not stored in the database. They are useful for displaying computed values that populate within the API response through hooks, etc.
Virtual fields are used to display data that is not stored in the database. They are useful for displaying computed values that populate within the APi response through hooks, etc.
Here are the available Virtual Fields:

View File

@@ -36,7 +36,7 @@ export const MyRadioField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. |
| **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing an `label` string and a `value` string. |
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`index`** | Build an [index](/docs/database/overview) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. |
@@ -82,7 +82,7 @@ export const MyRadioField: Field = {
}
```
The Radio Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Radio Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Property | Description |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------- |

View File

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

View File

@@ -54,7 +54,6 @@ export const MySelectField: Field = {
| **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`filterOptions`** | Dynamically filter which options are available based on the user, data, etc. [More details](#filterOptions) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/fields/relationship#linking-virtual-fields-with-relationships). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |
@@ -68,61 +67,6 @@ _\* An asterisk denotes that a property is required._
used as a GraphQL enum.
</Banner>
### filterOptions
Used to dynamically filter which options are available based on the user, data, etc.
Some examples of this might include:
- Restricting options based on a user's role, e.g. admin-only options
- Displaying different options based on the value of another field, e.g. a city/state selector
The result of `filterOptions` will determine:
- Which options are displayed in the Admin Panel
- Which options can be saved to the database
To do this, use the `filterOptions` property in your [Field Config](./overview):
```ts
import type { Field } from 'payload'
export const MySelectField: Field = {
// ...
// highlight-start
type: 'select',
options: [
{
label: 'One',
value: 'one',
},
{
label: 'Two',
value: 'two',
},
{
label: 'Three',
value: 'three',
},
],
filterOptions: ({ options, data }) =>
data.disallowOption1
? options.filter(
(option) =>
(typeof option === 'string' ? options : option.value) !== 'one',
)
: options,
// highlight-end
}
```
<Banner type="warning">
**Note:** This property is similar to `filterOptions` in
[Relationship](./relationship) or [Upload](./upload) fields, except that the
return value of this function is simply an array of options, not a query
constraint.
</Banner>
## Admin Options
To customize the appearance and behavior of the Select Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
@@ -139,13 +83,12 @@ export const MySelectField: Field = {
}
```
The Select Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Select Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Property | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **`isClearable`** | Set to `true` if you'd like this field to be clearable within the Admin UI. |
| **`isSortable`** | Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop. (Only works when `hasMany` is set to `true`) |
| **`placeholder`** | Define a custom text or function to replace the generic default placeholder |
## Example

View File

@@ -70,7 +70,7 @@ export const MyTextField: Field = {
}
```
The Text Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Text Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -67,7 +67,7 @@ export const MyTextareaField: Field = {
}
```
The Textarea Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
The Textarea Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
| Option | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- |

View File

@@ -1,103 +0,0 @@
---
title: Folders
label: Overview
order: 10
desc: Folders allow you to group documents across collections, and are a great way to organize your content.
keywords: folders, folder, content organization
---
Folders allow you to group documents across collections, and are a great way to organize your content. Folders are built on top of relationship fields, when you enable folders on a collection, Payload adds a hidden relationship field `folders`, that relates to a folder — or no folder. Folders also have the `folder` field, allowing folders to be nested within other folders.
The configuration for folders is done in two places, the collection config and the Payload config. The collection config is where you enable folders, and the Payload config is where you configure the global folder settings.
<Banner type="warning">
**Note:** The Folders feature is currently in beta and may be subject to
change in minor versions updates prior to being stable.
</Banner>
## Folder Configuration
On the payload config, you can configure the following settings under the `folders` property:
```ts
// Type definition
type RootFoldersConfiguration = {
/**
* An array of functions to be ran when the folder collection is initialized
* This allows plugins to modify the collection configuration
*/
collectionOverrides?: (({
collection,
}: {
collection: CollectionConfig
}) => CollectionConfig | Promise<CollectionConfig>)[]
/**
* Ability to view hidden fields and collections related to folders
*
* @default false
*/
debug?: boolean
/**
* The Folder field name
*
* @default "folder"
*/
fieldName?: string
/**
* Slug for the folder collection
*
* @default "payload-folders"
*/
slug?: string
}
```
```ts
// Example usage
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
folders: {
// highlight-start
debug: true, // optional
collectionOverrides: [
async ({ collection }) => {
return collection
},
], // optional
fieldName: 'folder', // optional
slug: 'payload-folders', // optional
// highlight-end
},
})
```
## Collection Configuration
To enable folders on a collection, you need to set the `admin.folders` property to `true` on the collection config. This will add a hidden relationship field to the collection that relates to a folder — or no folder.
```ts
// Type definition
type CollectionFoldersConfiguration = boolean
```
```ts
// Example usage
import { buildConfig } from 'payload'
const config = buildConfig({
collections: [
{
slug: 'pages',
// highlight-start
folders: true, // defaults to false
// highlight-end
},
],
})
```

View File

@@ -23,7 +23,7 @@ Let's see examples on how context can be used in the first two scenarios mention
### Passing Data Between Hooks
To pass data between hooks, you can assign values to context in an earlier hook in the lifecycle of a request and expect it in the context of a later hook.
To pass data between hooks, you can assign values to context in an earlier hook in the lifecycle of a request and expect it the context in a later hook.
For example:

View File

@@ -63,50 +63,19 @@ const config = buildConfig({
export default config
```
## Enabling Content Source Maps
Now in your Next.js app, you need to add the `encodeSourceMaps` query parameter to your API requests. This will tell Payload to include the Content Source Maps in the API response.
<Banner type="warning">
**Note:** For performance reasons, this should only be done when in draft mode
or on preview deployments.
</Banner>
#### REST API
If you're using the REST API, include the `?encodeSourceMaps=true` search parameter.
Now in your Next.js app, include the `?encodeSourceMaps=true` parameter in any of your API requests. For performance reasons, this should only be done when in draft mode or on preview deployments.
```ts
if (isDraftMode || process.env.VERCEL_ENV === 'preview') {
const res = await fetch(
`${process.env.NEXT_PUBLIC_PAYLOAD_CMS_URL}/api/pages?encodeSourceMaps=true&where[slug][equals]=${slug}`,
`${process.env.NEXT_PUBLIC_PAYLOAD_CMS_URL}/api/pages?where[slug][equals]=${slug}&encodeSourceMaps=true`,
)
}
```
#### Local API
If you're using the Local API, include the `encodeSourceMaps` via the `context` property.
```ts
if (isDraftMode || process.env.VERCEL_ENV === 'preview') {
const res = await payload.find({
collection: 'pages',
where: {
slug: {
equals: slug,
},
},
context: {
encodeSourceMaps: true,
},
})
}
```
And that's it! You are now ready to enter Edit Mode and begin visually editing your content.
## Edit Mode
#### Edit Mode
To see Content Link on your site, you first need to visit any preview deployment on Vercel and login using the Vercel Toolbar. When Content Source Maps are detected on the page, a pencil icon will appear in the toolbar. Clicking this icon will enable Edit Mode, highlighting all editable fields on the page in blue.
@@ -125,9 +94,7 @@ const { cleaned, encoded } = vercelStegaSplit(text)
### Blocks and array fields
All `blocks` and `array` fields by definition do not have plain text strings to encode. For this reason, they are automatically given an additional `_encodedSourceMap` property, which you can use to enable Content Link on entire _sections_ of your site.
You can then specify the editing container by adding the `data-vercel-edit-target` HTML attribute to any top-level element of your block.
All `blocks` and `array` fields by definition do not have plain text strings to encode. For this reason, they are given an additional `_encodedSourceMap` property, which you can use to enable Content Link on entire _sections_ of your site. You can then specify the editing container by adding the `data-vercel-edit-target` HTML attribute to any top-level element of your block.
```ts
<div data-vercel-edit-target>

View File

@@ -33,7 +33,7 @@ Simply add a task to the `jobs.tasks` array in your Payload config. A task consi
| `onSuccess` | Function to be executed if the task succeeds. |
| `retries` | Specify the number of times that this step should be retried if it fails. If this is undefined, the task will either inherit the retries from the workflow or have no retries. If this is 0, the task will not be retried. By default, this is undefined. |
The logic for the Task is defined in the `handler` - which can be defined as a function, or a path to a function. The `handler` will run once a worker picks up a Job that includes this task.
The logic for the Task is defined in the `handler` - which can be defined as a function, or a path to a function. The `handler` will run once a worker picks picks up a Job that includes this task.
It should return an object with an `output` key, which should contain the output of the task as you've defined.
@@ -213,7 +213,7 @@ export default buildConfig({
## Nested tasks
You can run sub-tasks within an existing task, by using the `tasks` or `inlineTask` arguments passed to the task `handler` function:
You can run sub-tasks within an existing task, by using the `tasks` or `ìnlineTask` arguments passed to the task `handler` function:
```ts
export default buildConfig({

View File

@@ -260,7 +260,7 @@ If you are using relationships or uploads in your front-end application, and you
{
// ...
// If your site is running on a different domain than your Payload server,
// This will allow requests to be made between the two domains
// This will allows requests to be made between the two domains
cors: [
'http://localhost:3001' // Your front-end application
],

View File

@@ -43,12 +43,13 @@ Setting up Live Preview is easy. This can be done either globally through the [R
The following options are available:
| Path | Description |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`url`** \* | String, or function that returns a string, pointing to your front-end application. This value is used as the iframe `src`. [More details](#url). |
| **`breakpoints`** | Array of breakpoints to be used as “device sizes” in the preview window. Each item appears as an option in the toolbar. [More details](#breakpoints). |
| **`collections`** | Array of collection slugs to enable Live Preview on. |
| **`globals`** | Array of global slugs to enable Live Preview on. |
| Path | Description |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`url`** \* | String, or function that returns a string, pointing to your front-end application. This value is used as the iframe `src`. [More details](#url). |
| **`breakpoints`** | Array of breakpoints to be used as “device sizes” in the preview window. Each item appears as an option in the toolbar. [More details](#breakpoints). |
| **`collections`** | Array of collection slugs to enable Live Preview on. |
| **`globals`** | Array of global slugs to enable Live Preview on. |
| **`defaultTab`** | Boolean to set Live Preview as the default tab when editing documents. Applies to all collections and globals where Live Preview is enabled. Defaults to `false`. |
_\* An asterisk denotes that a property is required._

View File

@@ -85,7 +85,6 @@ formBuilderPlugin({
checkbox: true,
number: true,
message: true,
date: false,
payment: false,
},
})
@@ -350,18 +349,6 @@ Maps to a `checkbox` input on your front-end. Used to collect a boolean value.
| `width` | string | The width of the field on the front-end. |
| `required` | checkbox | Whether or not the field is required when submitted. |
### Date
Maps to a `date` input on your front-end. Used to collect a date value.
| Property | Type | Description |
| -------------- | -------- | ---------------------------------------------------- |
| `name` | string | The name of the field. |
| `label` | string | The label of the field. |
| `defaultValue` | date | The default value of the field. |
| `width` | string | The width of the field on the front-end. |
| `required` | checkbox | Whether or not the field is required when submitted. |
### Number
Maps to a `number` input on your front-end. Used to collect a number.
@@ -434,42 +421,6 @@ formBuilderPlugin({
})
```
### Customizing the date field default value
You can custommise the default value of the date field and any other aspects of the date block in this way.
Note that the end submission source will be responsible for the timezone of the date. Payload only stores the date in UTC format.
```ts
import { fields as formFields } from '@payloadcms/plugin-form-builder'
// payload.config.ts
formBuilderPlugin({
fields: {
// date: true, // just enable it without any customizations
date: {
...formFields.date,
fields: [
...(formFields.date && 'fields' in formFields.date
? formFields.date.fields.map((field) => {
if ('name' in field && field.name === 'defaultValue') {
return {
...field,
timezone: true, // optionally enable timezone
admin: {
...field.admin,
description: 'This is a date field',
},
}
}
return field
})
: []),
],
},
},
})
```
## Email
This plugin relies on the [email configuration](../email/overview) defined in your Payload configuration. It will read from your config and attempt to send your emails using the credentials provided.

View File

@@ -309,3 +309,7 @@ import {
...
} from '@payloadcms/plugin-stripe/types';
```
## Examples
The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) contains an official [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommerce) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end. You can also check out [How to Build An E-Commerce Site With Next.js](https://payloadcms.com/blog/how-to-build-an-e-commerce-site-with-nextjs) post for a bit more context around this template.

View File

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

View File

@@ -6,14 +6,14 @@ desc: Converting between lexical richtext and HTML
keywords: lexical, richtext, html
---
## Rich Text to HTML
## Converting Rich Text to HTML
There are two main approaches to convert your Lexical-based rich text to HTML:
1. **Generate HTML on-demand (Recommended)**: Convert JSON to HTML wherever you need it, on-demand.
2. **Generate HTML within your Collection**: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API and may not work well with live preview.
### On-demand
### Generating HTML on-demand (Recommended)
To convert JSON to HTML on-demand, use the `convertLexicalToHTML` function from `@payloadcms/richtext-lexical/html`. Here's an example of how to use it in a React component in your frontend:
@@ -32,81 +32,61 @@ export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
}
```
#### Dynamic Population (Advanced)
### Converting Lexical Blocks
By default, `convertLexicalToHTML` expects fully populated data (e.g. uploads, links, etc.). If you need to dynamically fetch and populate those nodes, use the async variant, `convertLexicalToHTMLAsync`, from `@payloadcms/richtext-lexical/html-async`. You must provide a `populate` function:
If your rich text includes Lexical blocks, you need to provide a way to convert them to HTML. For example:
```tsx
'use client'
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
import type {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import React, { useEffect, useState } from 'react'
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const [html, setHTML] = useState<null | string>(null)
useEffect(() => {
async function convert() {
const html = await convertLexicalToHTMLAsync({
data,
populate: getRestPopulateFn({
apiURL: `http://localhost:3000/api`,
}),
})
setHTML(html)
}
void convert()
}, [data])
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
Using the REST populate function will send a separate request for each node. If you need to populate a large number of nodes, this may be slow. For improved performance on the server, you can use the `getPayloadPopulateFn` function:
```tsx
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import { getPayload } from 'payload'
import {
convertLexicalToHTML,
type HTMLConvertersFunction,
} from '@payloadcms/richtext-lexical/html'
import React from 'react'
import config from '../../config.js'
type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<MyTextBlock>
| SerializedInlineBlockNode<MyInlineBlock>
export const MyRSCComponent = async ({
data,
}: {
data: SerializedEditorState
}) => {
const payload = await getPayload({
config,
})
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({
defaultConverters,
}) => ({
...defaultConverters,
blocks: {
// Each key should match your block's slug
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
inlineBlocks: {
// Each key should match your inline block's slug
myInlineBlock: ({ node, providedStyleTag }) =>
`<span${providedStyleTag}>${node.fields.text}</span$>`,
},
})
const html = await convertLexicalToHTMLAsync({
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({
converters: htmlConverters,
data,
populate: await getPayloadPopulateFn({
currentDepth: 0,
depth: 1,
payload,
}),
})
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
### HTML field
### Outputting HTML from the Collection
The `lexicalHTMLField()` helper converts JSON to HTML and saves it in a field that is updated every time you read it via an `afterRead` hook. It's generally not recommended for two reasons:
1. It creates a column with duplicate content in another format.
2. In [client-side live preview](/docs/live-preview/client), it makes it not "live".
Consider using the [on-demand HTML converter above](/docs/rich-text/converting-html#on-demand-recommended) or the [JSX converter](/docs/rich-text/converting-jsx) unless you have a good reason.
To automatically generate HTML from the saved richText field in your Collection, use the `lexicalHTMLField()` helper. This approach converts the JSON to HTML using an `afterRead` hook. For instance:
```ts
import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
@@ -174,59 +154,74 @@ const Pages: CollectionConfig = {
}
```
## Blocks to HTML
### Generating HTML in Your Frontend with Dynamic Population (Advanced)
If your rich text includes Lexical blocks, you need to provide a way to convert them to HTML. For example:
By default, `convertLexicalToHTML` expects fully populated data (e.g. uploads, links, etc.). If you need to dynamically fetch and populate those nodes, use the async variant, `convertLexicalToHTMLAsync`, from `@payloadcms/richtext-lexical/html-async`. You must provide a `populate` function:
```tsx
'use client'
import type { MyInlineBlock, MyTextBlock } from '@/payload-types'
import type {
DefaultNodeTypes,
SerializedBlockNode,
SerializedInlineBlockNode,
} from '@payloadcms/richtext-lexical'
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
convertLexicalToHTML,
type HTMLConvertersFunction,
} from '@payloadcms/richtext-lexical/html'
import React from 'react'
type NodeTypes =
| DefaultNodeTypes
| SerializedBlockNode<MyTextBlock>
| SerializedInlineBlockNode<MyInlineBlock>
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({
defaultConverters,
}) => ({
...defaultConverters,
blocks: {
// Each key should match your block's slug
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
inlineBlocks: {
// Each key should match your inline block's slug
myInlineBlock: ({ node, providedStyleTag }) =>
`<span${providedStyleTag}>${node.fields.text}</span$>`,
},
})
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import React, { useEffect, useState } from 'react'
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
const html = convertLexicalToHTML({
converters: htmlConverters,
data,
})
const [html, setHTML] = useState<null | string>(null)
useEffect(() => {
async function convert() {
const html = await convertLexicalToHTMLAsync({
data,
populate: getRestPopulateFn({
apiURL: `http://localhost:3000/api`,
}),
})
setHTML(html)
}
return <div dangerouslySetInnerHTML={{ __html: html }} />
void convert()
}, [data])
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
## HTML to Richtext
Using the REST populate function will send a separate request for each node. If you need to populate a large number of nodes, this may be slow. For improved performance on the server, you can use the `getPayloadPopulateFn` function:
```tsx
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical'
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import { getPayload } from 'payload'
import React from 'react'
import config from '../../config.js'
export const MyRSCComponent = async ({
data,
}: {
data: SerializedEditorState
}) => {
const payload = await getPayload({
config,
})
const html = await convertLexicalToHTMLAsync({
data,
populate: await getPayloadPopulateFn({
currentDepth: 0,
depth: 1,
payload,
}),
})
return html && <div dangerouslySetInnerHTML={{ __html: html }} />
}
```
## Converting HTML to Richtext
If you need to convert raw HTML into a Lexical editor state, use `convertHTMLToLexical` from `@payloadcms/richtext-lexical`, along with the [editorConfigFactory to retrieve the editor config](/docs/rich-text/converters#retrieving-the-editor-config):

View File

@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and JSX
keywords: lexical, richtext, jsx
---
## Richtext to JSX
## Converting Richtext to JSX
To convert richtext to JSX, import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the richtext content to it:
@@ -28,7 +28,7 @@ The `RichText` component includes built-in converters for common Lexical nodes.
populated data to work correctly.
</Banner>
### Internal Links
### Converting Internal Links
By default, Payload doesn't know how to convert **internal** links to JSX, as it doesn't know what the corresponding URL of the internal link is. You'll notice that you get a "found internal link, but internalDocToHref is not provided" error in the console when you try to render content with internal links.
@@ -81,7 +81,7 @@ export const MyComponent: React.FC<{
}
```
### Lexical Blocks
### Converting Lexical Blocks
If your rich text includes custom Blocks or Inline Blocks, you must supply custom converters that match each block's slug. This converter is not included by default, as Payload doesn't know how to render your custom blocks.
@@ -133,9 +133,9 @@ export const MyComponent: React.FC<{
}
```
### Overriding Converters
### Overriding Default JSX Converters
You can override any of the default JSX converters by passing your custom converter, keyed to the node type, to the `converters` prop / the converters function.
You can override any of the default JSX converters by passing passing your custom converter, keyed to the node type, to the `converters` prop / the converters function.
Example - overriding the upload node converter to use next/image:

View File

@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and Markdown / MDX
keywords: lexical, richtext, markdown, md, mdx
---
## Richtext to Markdown
## Converting Richtext to Markdown
If you have access to the Payload Config and the [lexical editor config](/docs/rich-text/converters#retrieving-the-editor-config), you can convert the lexical editor state to Markdown with the following:
@@ -91,7 +91,7 @@ const Pages: CollectionConfig = {
}
```
## Markdown to Richtext
## Converting Markdown to Richtext
If you have access to the Payload Config and the [lexical editor config](/docs/rich-text/converters#retrieving-the-editor-config), you can convert Markdown to the lexical editor state with the following:

View File

@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and plaintext
keywords: lexical, richtext, plaintext, text
---
## Richtext to Plaintext
## Converting Richtext to Plaintext
Here's how you can convert richtext data to plaintext using `@payloadcms/richtext-lexical/plaintext`.

View File

@@ -142,33 +142,32 @@ import { CallToAction } from '../blocks/CallToAction'
Here's an overview of all the included features:
| Feature Name | Included by default | Description |
| ----------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`BoldFeature`** | Yes | Handles the bold text format |
| **`ItalicFeature`** | Yes | Handles the italic text format |
| **`UnderlineFeature`** | Yes | Handles the underline text format |
| **`StrikethroughFeature`** | Yes | Handles the strikethrough text format |
| **`SubscriptFeature`** | Yes | Handles the subscript text format |
| **`SuperscriptFeature`** | Yes | Handles the superscript text format |
| **`InlineCodeFeature`** | Yes | Handles the inline-code text format |
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
| **`ChecklistFeature`** | Yes | Adds checklists |
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
| **`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 |
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../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 |
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
| **`EXPERIMENTAL_TextStateFeature`** | No | Allows you to store key-value attributes within TextNodes and assign them inline styles. |
| Feature Name | Included by default | Description |
| ------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`BoldFeature`** | Yes | Handles the bold text format |
| **`ItalicFeature`** | Yes | Handles the italic text format |
| **`UnderlineFeature`** | Yes | Handles the underline text format |
| **`StrikethroughFeature`** | Yes | Handles the strikethrough text format |
| **`SubscriptFeature`** | Yes | Handles the subscript text format |
| **`SuperscriptFeature`** | Yes | Handles the superscript text format |
| **`InlineCodeFeature`** | Yes | Handles the inline-code text format |
| **`ParagraphFeature`** | Yes | Handles paragraphs. Since they are already a key feature of lexical itself, this Feature mainly handles the Slash and Add-Block menu entries for paragraphs |
| **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) |
| **`AlignFeature`** | Yes | Allows you to align text left, centered and right |
| **`IndentFeature`** | Yes | Allows you to indent text with the tab key |
| **`UnorderedListFeature`** | Yes | Adds unordered lists (ul) |
| **`OrderedListFeature`** | Yes | Adds ordered lists (ol) |
| **`ChecklistFeature`** | Yes | Adds checklists |
| **`LinkFeature`** | Yes | Allows you to create internal and external links |
| **`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 |
| **`HorizontalRuleFeature`** | Yes | Horizontal rules / separators. Basically displays an `<hr>` element |
| **`InlineToolbarFeature`** | Yes | The inline toolbar is the floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text |
| **`FixedToolbarFeature`** | No | This classic toolbar is pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. |
| **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../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 |
| **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. |
Notice how even the toolbars are features? That's how extensible our lexical editor is - you could theoretically create your own toolbar if you wanted to!

View File

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

View File

@@ -3,7 +3,7 @@ title: Autosave
label: Autosave
order: 30
desc: Using Payload's Draft functionality, you can configure your collections and globals to autosave changes as drafts, and publish only you're ready.
keywords: version history, revisions, audit log, draft, publish, autosave, Content Management System, cms, headless, javascript, node, react, nextjs
keywords: version history, revisions, audit log, draft, publish, autosave, Content Management System, cms, headless, javascript, node, react, nextjss
---
Extending on Payload's [Draft](/docs/versions/drafts) functionality, you can configure your collections and globals to autosave changes as drafts, and publish only you're ready. The Admin UI will automatically adapt to autosaving progress at an interval that you define, and will store all autosaved changes as a new Draft version. Never lose your work - and publish changes to the live document only when you're ready.

View File

@@ -74,13 +74,21 @@ export const rootEslintConfig = [
'no-console': 'off',
'perfectionist/sort-object-types': 'off',
'perfectionist/sort-objects': 'off',
'payload/no-relative-monorepo-imports': 'off',
},
},
]
export default [
...rootEslintConfig,
{
languageOptions: {
parserOptions: {
...rootParserOptions,
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
files: ['packages/eslint-config/**/*.ts'],
rules: {

View File

@@ -1,7 +1,7 @@
import type { CollectionConfig } from 'payload/types'
import { admins } from './access/admins'
import { adminsAndUser } from './access/adminsAndUser'
import adminsAndUser from './access/adminsAndUser'
import { anyone } from './access/anyone'
import { checkRole } from './access/checkRole'
import { loginAfterCreate } from './hooks/loginAfterCreate'
@@ -25,7 +25,6 @@ export const Users: CollectionConfig = {
create: anyone,
update: adminsAndUser,
delete: admins,
unlock: admins,
admin: ({ req: { user } }) => checkRole(['admin'], user),
},
hooks: {

View File

@@ -1,4 +1,4 @@
import type { Access } from 'payload'
import type { Access } from 'payload/config'
import { checkRole } from './checkRole'

View File

@@ -1,17 +1,19 @@
import type { Access } from 'payload'
import type { Access } from 'payload/config'
import { checkRole } from './checkRole'
export const adminsAndUser: Access = ({ req: { user } }) => {
const adminsAndUser: Access = ({ req: { user } }) => {
if (user) {
if (checkRole(['admin'], user)) {
return true
}
return {
id: { equals: user.id },
id: user.id,
}
}
return false
}
export default adminsAndUser

View File

@@ -1,3 +1,3 @@
import type { Access } from 'payload'
import type { Access } from 'payload/config'
export const anyone: Access = () => true

View File

@@ -1,6 +1,6 @@
import type { User } from '../../payload-types'
export const checkRole = (allRoles: User['roles'] = [], user: User | null = null): boolean => {
export const checkRole = (allRoles: User['roles'] = [], user: User = undefined): boolean => {
if (user) {
if (
allRoles.some((role) => {
@@ -8,9 +8,8 @@ export const checkRole = (allRoles: User['roles'] = [], user: User | null = null
return individualRole === role
})
})
) {
return true
}
)
{return true}
}
return false

View File

@@ -1,4 +1,4 @@
import type { FieldHook } from 'payload'
import type { FieldHook } from 'payload/types'
import type { User } from '../../payload-types'

View File

@@ -1,6 +1,7 @@
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import path from 'path'
import express from 'express'
import { buildConfig } from 'payload'
import { fileURLToPath } from 'url'

View File

@@ -1,4 +1,5 @@
import express from 'express'
import type { Request, Response } from 'express'
import { parse } from 'url'
import next from 'next'

View File

@@ -58,7 +58,7 @@ See the [Collections](https://payloadcms.com/docs/configuration/collections) doc
}
```
For more details on how to extend this functionality, see the [Live Preview](https://payloadcms.com/docs/live-preview/overview) docs.
For more details on how to extend this functionality, see the [Live Preview](https://payloadcms.com/docs/live-preview) docs.
## Front-end

View File

@@ -36,7 +36,7 @@ export const home: Partial<Page> = {
type: 'link',
children: [{ text: 'Live Preview' }],
newTab: true,
url: 'https://payloadcms.com/docs/live-preview/overview',
url: 'https://payloadcms.com/docs/live-preview',
},
{
text: ' you can edit this page in the admin panel and see the changes reflected here in real time.',

View File

@@ -1,11 +1,11 @@
import { BeforeSync, DocToSync } from '@payloadcms/plugin-search/types'
export const beforeSyncWithSearch: BeforeSync = async ({ req, originalDoc, searchDoc }) => {
export const beforeSyncWithSearch: BeforeSync = async ({ originalDoc, searchDoc, payload }) => {
const {
doc: { relationTo: collection },
} = searchDoc
const { slug, id, categories, title, meta } = originalDoc
const { slug, id, categories, title, meta, excerpt } = originalDoc
const modifiedDoc: DocToSync = {
...searchDoc,
@@ -20,40 +20,24 @@ export const beforeSyncWithSearch: BeforeSync = async ({ req, originalDoc, searc
}
if (categories && Array.isArray(categories) && categories.length > 0) {
const populatedCategories: { id: string | number; title: string }[] = []
for (const category of categories) {
if (!category) {
continue
}
// get full categories and keep a flattened copy of their most important properties
try {
const mappedCategories = categories.map((category) => {
const { id, title } = category
if (typeof category === 'object') {
populatedCategories.push(category)
continue
}
const doc = await req.payload.findByID({
collection: 'categories',
id: category,
disableErrors: true,
depth: 0,
select: { title: true },
req,
return {
relationTo: 'categories',
id,
title,
}
})
if (doc !== null) {
populatedCategories.push(doc)
} else {
console.error(
`Failed. Category not found when syncing collection '${collection}' with id: '${id}' to search.`,
)
}
modifiedDoc.categories = mappedCategories
} catch (err) {
console.error(
`Failed. Category not found when syncing collection '${collection}' with id: '${id}' to search.`,
)
}
modifiedDoc.categories = populatedCategories.map((each) => ({
relationTo: 'categories',
categoryID: String(each.id),
title: each.title,
}))
}
return modifiedDoc

View File

@@ -52,7 +52,7 @@ export const searchFields: Field[] = [
type: 'text',
},
{
name: 'categoryID',
name: 'id',
type: 'text',
},
{

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.39.1",
"version": "3.35.1",
"private": true,
"type": "module",
"scripts": {
@@ -74,9 +74,9 @@
"docker:start": "docker compose -f test/docker-compose.yml up -d",
"docker:stop": "docker compose -f test/docker-compose.yml down",
"force:build": "pnpm run build:core:force",
"lint": "turbo run lint --log-order=grouped --continue",
"lint": "turbo run lint --concurrency 1 --continue",
"lint-staged": "lint-staged",
"lint:fix": "turbo run lint:fix --log-order=grouped --continue",
"lint:fix": "turbo run lint:fix --concurrency 1 --continue",
"obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
"prepare": "husky",
"prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",

View File

@@ -0,0 +1,18 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{
languageOptions: {
parserOptions: {
...rootParserOptions,
tsconfigRootDir: import.meta.dirname,
},
},
},
]
export default index

View File

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

View File

@@ -0,0 +1,19 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{
ignores: ['bin/cli.js'],
languageOptions: {
parserOptions: {
...rootParserOptions,
tsconfigRootDir: import.meta.dirname,
},
},
},
]
export default index

View File

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

View File

@@ -0,0 +1,8 @@
import { readFileSync } from 'fs'
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const packageJson = JSON.parse(readFileSync(path.resolve(dirname, '../../package.json'), 'utf-8'))
export const PACKAGE_VERSION = packageJson.version

View File

@@ -10,7 +10,6 @@ import type { CliArgs, DbType, ProjectExample, ProjectTemplate } from '../types.
import { createProject } from './create-project.js'
import { dbReplacements } from './replacements.js'
import { getValidTemplates } from './templates.js'
import { manageEnvFiles } from './manage-env-files.js'
describe('createProject', () => {
let projectDir: string
@@ -155,75 +154,5 @@ describe('createProject', () => {
expect(content).toContain(dbReplacement.configReplacement().join('\n'))
})
})
describe('managing env files', () => {
it('updates .env files without overwriting existing data', async () => {
const envFilePath = path.join(projectDir, '.env')
const envExampleFilePath = path.join(projectDir, '.env.example')
fse.ensureDirSync(projectDir)
fse.ensureFileSync(envFilePath)
fse.ensureFileSync(envExampleFilePath)
const initialEnvContent = `CUSTOM_VAR=custom-value\nDATABASE_URI=old-connection\n`
const initialEnvExampleContent = `CUSTOM_VAR=custom-value\nDATABASE_URI=old-connection\nPAYLOAD_SECRET=YOUR_SECRET_HERE\n`
fse.writeFileSync(envFilePath, initialEnvContent)
fse.writeFileSync(envExampleFilePath, initialEnvExampleContent)
await manageEnvFiles({
cliArgs: {
'--debug': true,
} as CliArgs,
databaseType: 'mongodb',
databaseUri: 'mongodb://localhost:27017/test',
payloadSecret: 'test-secret',
projectDir,
template: undefined,
})
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
expect(updatedEnvContent).toContain('CUSTOM_VAR=custom-value')
expect(updatedEnvContent).toContain('DATABASE_URI=mongodb://localhost:27017/test')
expect(updatedEnvContent).toContain('PAYLOAD_SECRET=test-secret')
const updatedEnvExampleContent = fse.readFileSync(envExampleFilePath, 'utf-8')
expect(updatedEnvExampleContent).toContain('CUSTOM_VAR=custom-value')
expect(updatedEnvContent).toContain('DATABASE_URI=mongodb://localhost:27017/test')
expect(updatedEnvContent).toContain('PAYLOAD_SECRET=test-secret')
})
it('creates .env and .env.example if they do not exist', async () => {
const envFilePath = path.join(projectDir, '.env')
const envExampleFilePath = path.join(projectDir, '.env.example')
fse.ensureDirSync(projectDir)
if (fse.existsSync(envFilePath)) fse.removeSync(envFilePath)
if (fse.existsSync(envExampleFilePath)) fse.removeSync(envExampleFilePath)
await manageEnvFiles({
cliArgs: {
'--debug': true,
} as CliArgs,
databaseUri: '',
payloadSecret: '',
projectDir,
template: undefined,
})
expect(fse.existsSync(envFilePath)).toBe(true)
expect(fse.existsSync(envExampleFilePath)).toBe(true)
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
expect(updatedEnvContent).toContain('DATABASE_URI=your-connection-string-here')
expect(updatedEnvContent).toContain('PAYLOAD_SECRET=YOUR_SECRET_HERE')
const updatedEnvExampleContent = fse.readFileSync(envExampleFilePath, 'utf-8')
expect(updatedEnvExampleContent).toContain('DATABASE_URI=your-connection-string-here')
expect(updatedEnvExampleContent).toContain('PAYLOAD_SECRET=YOUR_SECRET_HERE')
})
})
})
})

View File

@@ -6,57 +6,66 @@ import type { CliArgs, DbType, ProjectTemplate } from '../types.js'
import { debug, error } from '../utils/log.js'
import { dbChoiceRecord } from './select-db.js'
const updateEnvExampleVariables = (
contents: string,
databaseType: DbType | undefined,
payloadSecret?: string,
databaseUri?: string,
): string => {
const seenKeys = new Set<string>()
const updatedEnv = contents
const updateEnvExampleVariables = (contents: string, databaseType: DbType | undefined): string => {
return contents
.split('\n')
.map((line) => {
if (line.startsWith('#') || !line.includes('=')) {
return line
return line // Preserve comments and unrelated lines
}
const [key] = line.split('=')
if (!key) {
return
}
if (key === 'DATABASE_URI' || key === 'POSTGRES_URL' || key === 'MONGODB_URI') {
const dbChoice = databaseType ? dbChoiceRecord[databaseType] : null
if (dbChoice) {
const placeholderUri = databaseUri
? databaseUri
: `${dbChoice.dbConnectionPrefix}your-database-name${dbChoice.dbConnectionSuffix || ''}`
line =
databaseType === 'vercel-postgres'
? `POSTGRES_URL=${placeholderUri}`
: `DATABASE_URI=${placeholderUri}`
const placeholderUri = `${dbChoice.dbConnectionPrefix}your-database-name${
dbChoice.dbConnectionSuffix || ''
}`
return databaseType === 'vercel-postgres'
? `POSTGRES_URL=${placeholderUri}`
: `DATABASE_URI=${placeholderUri}`
}
return `DATABASE_URI=your-database-connection-here` // Fallback
}
if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') {
line = `PAYLOAD_SECRET=${payloadSecret || 'YOUR_SECRET_HERE'}`
return `PAYLOAD_SECRET=YOUR_SECRET_HERE`
}
// handles dupes
if (seenKeys.has(key)) {
return null
}
seenKeys.add(key)
return line
})
.filter(Boolean)
.reverse()
.join('\n')
}
return updatedEnv
const generateEnvContent = (
existingEnv: string,
databaseType: DbType | undefined,
databaseUri: string,
payloadSecret: string,
): string => {
const dbKey = databaseType === 'vercel-postgres' ? 'POSTGRES_URL' : 'DATABASE_URI'
const envVars: Record<string, string> = {}
existingEnv
.split('\n')
.filter((line) => line.includes('=') && !line.startsWith('#'))
.forEach((line) => {
const [key, value] = line.split('=')
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
envVars[key] = value
})
// Override specific keys
envVars[dbKey] = databaseUri
envVars['PAYLOAD_SECRET'] = payloadSecret
// Rebuild content
return Object.entries(envVars)
.map(([key, value]) => `${key}=${value}`)
.join('\n')
}
/** Parse and swap .env.example values and write .env */
@@ -79,71 +88,42 @@ export async function manageEnvFiles(args: {
const envExamplePath = path.join(projectDir, '.env.example')
const envPath = path.join(projectDir, '.env')
const emptyEnvContent = `# Added by Payload\nDATABASE_URI=your-connection-string-here\nPAYLOAD_SECRET=YOUR_SECRET_HERE\n`
try {
let updatedExampleContents: string
if (template?.type === 'plugin') {
if (debugFlag) {
debug(`plugin template detected - no .env added .env.example added`)
// Update .env.example
if (template?.type === 'starter') {
if (!fs.existsSync(envExamplePath)) {
error(`.env.example file not found at ${envExamplePath}`)
process.exit(1)
}
return
}
if (!fs.existsSync(envExamplePath)) {
updatedExampleContents = updateEnvExampleVariables(
emptyEnvContent,
databaseType,
payloadSecret,
databaseUri,
)
await fs.writeFile(envExamplePath, updatedExampleContents)
if (debugFlag) {
debug(`.env.example file successfully created`)
}
} else {
const envExampleContents = await fs.readFile(envExamplePath, 'utf8')
const mergedEnvs = envExampleContents + '\n' + emptyEnvContent
updatedExampleContents = updateEnvExampleVariables(
mergedEnvs,
databaseType,
payloadSecret,
databaseUri,
)
updatedExampleContents = updateEnvExampleVariables(envExampleContents, databaseType)
await fs.writeFile(envExamplePath, updatedExampleContents.trimEnd() + '\n')
await fs.writeFile(envExamplePath, updatedExampleContents)
if (debugFlag) {
debug(`.env.example file successfully updated`)
}
} else {
updatedExampleContents = `# Added by Payload\nDATABASE_URI=your-connection-string-here\nPAYLOAD_SECRET=YOUR_SECRET_HERE\n`
await fs.writeFile(envExamplePath, updatedExampleContents.trimEnd() + '\n')
}
if (!fs.existsSync(envPath)) {
const envContent = updateEnvExampleVariables(
emptyEnvContent,
databaseType,
payloadSecret,
databaseUri,
)
await fs.writeFile(envPath, envContent)
// Merge existing variables and create or update .env
const envExampleContents = await fs.readFile(envExamplePath, 'utf8')
const envContent = generateEnvContent(
envExampleContents,
databaseType,
databaseUri,
payloadSecret,
)
await fs.writeFile(envPath, `# Added by Payload\n${envContent.trimEnd()}\n`)
if (debugFlag) {
debug(`.env file successfully created`)
}
} else {
const envContents = await fs.readFile(envPath, 'utf8')
const mergedEnvs = envContents + '\n' + emptyEnvContent
const updatedEnvContents = updateEnvExampleVariables(
mergedEnvs,
databaseType,
payloadSecret,
databaseUri,
)
await fs.writeFile(envPath, updatedEnvContents)
if (debugFlag) {
debug(`.env file successfully updated`)
}
if (debugFlag) {
debug(`.env file successfully created or updated`)
}
} catch (err: unknown) {
error('Unable to manage environment files')

View File

@@ -1,8 +1,9 @@
import type { ProjectTemplate } from '../types.js'
import { error, info } from '../utils/log.js'
import { PACKAGE_VERSION } from './constants.js'
export function validateTemplate({ templateName }: { templateName: string }): boolean {
export function validateTemplate(templateName: string): boolean {
const validTemplates = getValidTemplates()
if (!validTemplates.map((t) => t.name).includes(templateName)) {
error(`'${templateName}' is not a valid template.`)
@@ -19,13 +20,13 @@ export function getValidTemplates(): ProjectTemplate[] {
name: 'blank',
type: 'starter',
description: 'Blank 3.0 Template',
url: `https://github.com/payloadcms/payload/templates/blank#main`,
url: `https://github.com/payloadcms/payload/templates/blank#v${PACKAGE_VERSION}`,
},
{
name: 'website',
type: 'starter',
description: 'Website Template',
url: `https://github.com/payloadcms/payload/templates/website#main`,
url: `https://github.com/payloadcms/payload/templates/website#v${PACKAGE_VERSION}`,
},
{
name: 'plugin',

View File

@@ -1,3 +1,4 @@
import execa from 'execa'
import fse from 'fs-extra'
import { fileURLToPath } from 'node:url'
import path from 'path'
@@ -8,7 +9,6 @@ const dirname = path.dirname(filename)
import type { NextAppDetails } from '../types.js'
import { copyRecursiveSync } from '../utils/copy-recursive-sync.js'
import { getLatestPackageVersion } from '../utils/getLatestPackageVersion.js'
import { info } from '../utils/log.js'
import { getPackageManager } from './get-package-manager.js'
import { installPackages } from './install-packages.js'
@@ -36,8 +36,15 @@ export async function updatePayloadInProject(
const packageManager = await getPackageManager({ projectDir })
// Fetch latest Payload version
const latestPayloadVersion = await getLatestPackageVersion({ packageName: 'payload' })
// Fetch latest Payload version from npm
const { exitCode: getLatestVersionExitCode, stdout: latestPayloadVersion } = await execa('npm', [
'show',
'payload',
'version',
])
if (getLatestVersionExitCode !== 0) {
throw new Error('Failed to fetch latest Payload version')
}
if (payloadVersion === latestPayloadVersion) {
return { message: `Payload v${payloadVersion} is already up to date.`, success: true }

View File

@@ -8,6 +8,7 @@ import path from 'path'
import type { CliArgs } from './types.js'
import { configurePayloadConfig } from './lib/configure-payload-config.js'
import { PACKAGE_VERSION } from './lib/constants.js'
import { createProject } from './lib/create-project.js'
import { parseExample } from './lib/examples.js'
import { generateSecret } from './lib/generate-secret.js'
@@ -19,7 +20,6 @@ import { parseTemplate } from './lib/parse-template.js'
import { selectDb } from './lib/select-db.js'
import { getValidTemplates, validateTemplate } from './lib/templates.js'
import { updatePayloadInProject } from './lib/update-payload-in-project.js'
import { getLatestPackageVersion } from './utils/getLatestPackageVersion.js'
import { debug, error, info } from './utils/log.js'
import {
feedbackOutro,
@@ -78,18 +78,13 @@ export class Main {
async init(): Promise<void> {
try {
const debugFlag = this.args['--debug']
const LATEST_VERSION = await getLatestPackageVersion({
debug: debugFlag,
packageName: 'payload',
})
if (this.args['--help']) {
helpMessage()
process.exit(0)
}
const debugFlag = this.args['--debug']
// eslint-disable-next-line no-console
console.log('\n')
p.intro(chalk.bgCyan(chalk.black(' create-payload-app ')))
@@ -205,7 +200,7 @@ export class Main {
const templateArg = this.args['--template']
if (templateArg) {
const valid = validateTemplate({ templateName: templateArg })
const valid = validateTemplate(templateArg)
if (!valid) {
helpMessage()
process.exit(1)
@@ -235,7 +230,7 @@ export class Main {
}
if (debugFlag) {
debug(`Using ${exampleArg ? 'examples' : 'templates'} from git tag: v${LATEST_VERSION}`)
debug(`Using ${exampleArg ? 'examples' : 'templates'} from git tag: v${PACKAGE_VERSION}`)
}
if (!exampleArg) {

View File

@@ -1,34 +0,0 @@
/**
* Fetches the latest version of a package from the NPM registry.
*
* Used in determining the latest version of Payload to use in the generated templates.
*/
export async function getLatestPackageVersion({
debug = false,
packageName = 'payload',
}: {
debug?: boolean
/**
* Package name to fetch the latest version for based on the NPM registry URL
*
* Eg. for `'payload'`, it will fetch the version from `https://registry.npmjs.org/payload`
*
* @default 'payload'
*/
packageName?: string
}) {
try {
const response = await fetch(`https://registry.npmjs.org/${packageName}`)
const data = await response.json()
const latestVersion = data['dist-tags'].latest
if (debug) {
console.log(`Found latest version of ${packageName}: ${latestVersion}`)
}
return latestVersion
} catch (error) {
console.error('Error fetching Payload version:', error)
throw error
}
}

View File

@@ -0,0 +1,18 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{
languageOptions: {
parserOptions: {
...rootParserOptions,
tsconfigRootDir: import.meta.dirname,
},
},
},
]
export default index

View File

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

View File

@@ -372,61 +372,36 @@ const group: FieldSchemaGenerator<GroupField> = (
buildSchemaOptions,
parentIsLocalized,
): void => {
if (fieldAffectsData(field)) {
const formattedBaseSchema = formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized })
const formattedBaseSchema = formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized })
// carry indexSortableFields through to versions if drafts enabled
const indexSortableFields =
buildSchemaOptions.indexSortableFields &&
field.name === 'version' &&
buildSchemaOptions.draftsEnabled
// carry indexSortableFields through to versions if drafts enabled
const indexSortableFields =
buildSchemaOptions.indexSortableFields &&
field.name === 'version' &&
buildSchemaOptions.draftsEnabled
const baseSchema: SchemaTypeOptions<any> = {
...formattedBaseSchema,
type: buildSchema({
buildSchemaOptions: {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
indexSortableFields,
options: {
_id: false,
id: false,
minimize: false,
},
const baseSchema: SchemaTypeOptions<any> = {
...formattedBaseSchema,
type: buildSchema({
buildSchemaOptions: {
disableUnique: buildSchemaOptions.disableUnique,
draftsEnabled: buildSchemaOptions.draftsEnabled,
indexSortableFields,
options: {
_id: false,
id: false,
minimize: false,
},
configFields: field.fields,
parentIsLocalized: parentIsLocalized || field.localized,
payload,
}),
}
schema.add({
[field.name]: localizeSchema(
field,
baseSchema,
payload.config.localization,
parentIsLocalized,
),
})
} else {
field.fields.forEach((subField) => {
if (fieldIsVirtual(subField)) {
return
}
const addFieldSchema = getSchemaGenerator(subField.type)
if (addFieldSchema) {
addFieldSchema(
subField,
schema,
payload,
buildSchemaOptions,
(parentIsLocalized || field.localized) ?? false,
)
}
})
},
configFields: field.fields,
parentIsLocalized: parentIsLocalized || field.localized,
payload,
}),
}
schema.add({
[field.name]: localizeSchema(field, baseSchema, payload.config.localization, parentIsLocalized),
})
}
const json: FieldSchemaGenerator<JSONField> = (

View File

@@ -20,6 +20,7 @@ type SearchParam = {
const subQueryOptions = {
lean: true,
limit: 50,
}
/**
@@ -183,7 +184,7 @@ export async function buildSearchParam({
select[joinPath] = true
}
const result = await SubModel.find(subQuery).lean().select(select)
const result = await SubModel.find(subQuery).lean().limit(50).select(select)
const $in: unknown[] = []

View File

@@ -57,8 +57,12 @@ const relationshipSort = ({
return false
}
for (let i = 0; i < segments.length; i++) {
const segment = segments[i]
for (const [i, segment] of segments.entries()) {
if (versions && i === 0 && segment === 'version') {
segments.shift()
continue
}
const field = currentFields.find((each) => each.name === segment)
if (!field) {
@@ -67,10 +71,6 @@ const relationshipSort = ({
if ('fields' in field) {
currentFields = field.flattenedFields
if (field.name === 'version' && versions && i === 0) {
segments.shift()
i--
}
} else if (
(field.type === 'relationship' || field.type === 'upload') &&
i !== segments.length - 1
@@ -106,7 +106,7 @@ const relationshipSort = ({
as: `__${path}`,
foreignField: '_id',
from: foreignCollection.Model.collection.name,
localField: versions ? `version.${relationshipPath}` : relationshipPath,
localField: relationshipPath,
pipeline: [
{
$project: {
@@ -150,18 +150,6 @@ export const buildSortParam = ({
sort = [sort]
}
// In the case of Mongo, when sorting by a field that is not unique, the results are not guaranteed to be in the same order each time.
// So we add a fallback sort to ensure that the results are always in the same order.
let fallbackSort = '-id'
if (timestamps) {
fallbackSort = '-createdAt'
}
if (!(sort.includes(fallbackSort) || sort.includes(fallbackSort.replace('-', '')))) {
sort.push(fallbackSort)
}
const sorting = sort.reduce<Record<string, string>>((acc, item) => {
let sortProperty: string
let sortDirection: SortDirection

View File

@@ -105,7 +105,6 @@ export const sanitizeQueryValue = ({
| undefined => {
let formattedValue = val
let formattedOperator = operator
if (['array', 'blocks', 'group', 'tab'].includes(field.type) && path.includes('.')) {
const segments = path.split('.')
segments.shift()

View File

@@ -151,7 +151,6 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
query: versionQuery,
session: paginationOptions.options?.session ?? undefined,
sort: paginationOptions.sort as object,
sortAggregation,
useEstimatedCount: paginationOptions.useEstimatedCount,
})
} else {

View File

@@ -128,6 +128,7 @@ const traverseFields = ({
break
}
case 'blocks': {
const blocksSelect = select[field.name] as SelectType

View File

@@ -425,7 +425,6 @@ export const transform = ({
for (const locale of config.localization.localeCodes) {
sanitizeDate({
field,
locale,
ref: fieldRef,
value: fieldRef[locale],
})

View File

@@ -0,0 +1,18 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{
languageOptions: {
parserOptions: {
...rootParserOptions,
tsconfigRootDir: import.meta.dirname,
},
},
},
]
export default index

View File

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

View File

@@ -0,0 +1,18 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{
languageOptions: {
parserOptions: {
...rootParserOptions,
tsconfigRootDir: import.meta.dirname,
},
},
},
]
export default index

View File

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

View File

@@ -16,7 +16,7 @@ export const countDistinct: CountDistinct = async function countDistinct(
})
.from(this.tables[tableName])
.where(where)
return Number(countResult?.[0]?.count ?? 0)
return Number(countResult[0]?.count)
}
let query: SQLiteSelect = db
@@ -39,5 +39,5 @@ export const countDistinct: CountDistinct = async function countDistinct(
// Instead, COUNT (GROUP BY id) can be used which is still slower than COUNT(*) but acceptable.
const countResult = await query
return Number(countResult?.[0]?.count ?? 0)
return Number(countResult[0]?.count)
}

View File

@@ -0,0 +1,18 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{
languageOptions: {
parserOptions: {
...rootParserOptions,
tsconfigRootDir: import.meta.dirname,
},
},
},
]
export default index

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