Compare commits
102 Commits
feat/folde
...
chore/stri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbee9c4c4a | ||
|
|
ba011b8933 | ||
|
|
193c051c67 | ||
|
|
e7749468c2 | ||
|
|
319d3355de | ||
|
|
115af04406 | ||
|
|
2b40e0f21f | ||
|
|
30dd9a23a3 | ||
|
|
ec115c6eca | ||
|
|
c639c5f278 | ||
|
|
05eeddba7c | ||
|
|
08a6f88a4b | ||
|
|
4b3f1b9c92 | ||
|
|
ede5c671b8 | ||
|
|
684c43604a | ||
|
|
8199a7d32a | ||
|
|
6f8cff7764 | ||
|
|
89ced5ec6b | ||
|
|
836fd86090 | ||
|
|
7c094dc572 | ||
|
|
c83e791014 | ||
|
|
6119d89fa5 | ||
|
|
d5611953a7 | ||
|
|
71df378fb0 | ||
|
|
5e3a94bbc9 | ||
|
|
3670886bee | ||
|
|
6888f13f27 | ||
|
|
12395e497b | ||
|
|
a17d84e570 | ||
|
|
ca6f849b53 | ||
|
|
7e873a9d63 | ||
|
|
d85909e5ae | ||
|
|
699af8dc5b | ||
|
|
0c0b0fe0f8 | ||
|
|
bfdcb51793 | ||
|
|
ca26402377 | ||
|
|
3022cab8ac | ||
|
|
8a7ac784c4 | ||
|
|
54a04840c7 | ||
|
|
f2b54b5b43 | ||
|
|
4a41369a00 | ||
|
|
7fa879c3a0 | ||
|
|
166dafe05e | ||
|
|
68ba24d91f | ||
|
|
20f7017758 | ||
|
|
0204f0dcbc | ||
|
|
032375b016 | ||
|
|
8448e5b6b6 | ||
|
|
d6f6b05d77 | ||
|
|
feb7e082af | ||
|
|
dfa0974894 | ||
|
|
f2b6c4a707 | ||
|
|
b61ef13481 | ||
|
|
1731dd7c36 | ||
|
|
bd2571c68f | ||
|
|
e2f7889d72 | ||
|
|
c010d51543 | ||
|
|
293cdc1b50 | ||
|
|
06fbc0705c | ||
|
|
5a758810aa | ||
|
|
64443d83ec | ||
|
|
feeee19407 | ||
|
|
e9cda1e121 | ||
|
|
842d1845e1 | ||
|
|
11a4a20f0f | ||
|
|
bc43982cfc | ||
|
|
d83b2bf3fa | ||
|
|
f75d62c79b | ||
|
|
45f4c5c22c | ||
|
|
071c61fe49 | ||
|
|
cceb793257 | ||
|
|
1b1e36e2df | ||
|
|
6b6948f92c | ||
|
|
9ef51a7cf3 | ||
|
|
0f7dc38012 | ||
|
|
c720ce3c08 | ||
|
|
3a73a67ef4 | ||
|
|
4c6fde0e89 | ||
|
|
c1c0db3b01 | ||
|
|
00667faf8d | ||
|
|
898e97ed17 | ||
|
|
8142a00da6 | ||
|
|
08a3dfbbcb | ||
|
|
fc83823e5d | ||
|
|
2a41d3fbb1 | ||
|
|
c772a3207c | ||
|
|
c701dd41a9 | ||
|
|
4dfb2d24bb | ||
|
|
230128b92e | ||
|
|
23f42040ab | ||
|
|
8596ac5694 | ||
|
|
324daff553 | ||
|
|
22b1858ee8 | ||
|
|
2ab8e2e194 | ||
|
|
1235a183ff | ||
|
|
81d333f4b0 | ||
|
|
4fe3423e54 | ||
|
|
e8c2b15e2b | ||
|
|
3127d6ad6d | ||
|
|
72ab319d37 | ||
|
|
180ef3a49d | ||
|
|
1066b434c3 |
6
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
6
.github/ISSUE_TEMPLATE/1.bug_report_v3.yml
vendored
@@ -43,6 +43,7 @@ body:
|
||||
- 'plugin: cloud'
|
||||
- 'plugin: cloud-storage'
|
||||
- 'plugin: form-builder'
|
||||
- 'plugin: multi-tenant'
|
||||
- 'plugin: nested-docs'
|
||||
- 'plugin: richtext-lexical'
|
||||
- 'plugin: richtext-slate'
|
||||
@@ -59,10 +60,7 @@ body:
|
||||
label: Environment Info
|
||||
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions. Please avoid using "latest"—specific version numbers help us accurately diagnose and resolve issues.
|
||||
render: text
|
||||
placeholder: |
|
||||
Payload:
|
||||
Node.js:
|
||||
Next.js:
|
||||
placeholder: Run `pnpm payload info` in your terminal and paste the output here.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@ package-lock.json
|
||||
dist
|
||||
/.idea/*
|
||||
!/.idea/runConfigurations
|
||||
/.idea/runConfigurations/_template*
|
||||
!/.idea/payload.iml
|
||||
|
||||
# Custom actions
|
||||
|
||||
@@ -87,41 +87,43 @@ You can run the entire test suite using `pnpm test`. If you wish to only run e2e
|
||||
|
||||
By default, `pnpm test:int` will only run int test against MongoDB. To run int tests against postgres, you can use `pnpm test:int:postgres`. You will have to have postgres installed on your system for this to work.
|
||||
|
||||
### Commits
|
||||
### Pull Requests
|
||||
|
||||
We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for our commit messages. Please follow this format when creating commits. Here are some examples:
|
||||
For all Pull Requests, you should be extremely descriptive about both your problem and proposed solution. If there are any affected open or closed issues, please leave the issue number in your PR description.
|
||||
|
||||
- `feat: adds new feature`
|
||||
- `fix: fixes bug`
|
||||
- `docs: adds documentation`
|
||||
- `chore: does chore`
|
||||
All commits within a PR are squashed when merged, using the PR title as the commit message. For that reason, please use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for your PR titles.
|
||||
|
||||
Here's a breakdown of the format. At the top-level, we use the following types to categorize our commits:
|
||||
Here are some examples:
|
||||
|
||||
- `feat`: new feature that adds functionality. These are automatically added to the changelog when creating new releases.
|
||||
- `fix`: a fix to an existing feature. These are automatically added to the changelog when creating new releases.
|
||||
- `docs`: changes to [docs](./docs) only. These do not appear in the changelog.
|
||||
- `chore`: changes to code that is neither a fix nor a feature (e.g. refactoring, adding tests, etc.). These do not appear in the changelog.
|
||||
- `feat: add new feature`
|
||||
- `fix: fix bug`
|
||||
- `docs: add documentation`
|
||||
- `test: add/fix tests`
|
||||
- `refactor: refactor code`
|
||||
- `chore: anything that does not fit into the above categories`
|
||||
|
||||
If applicable, you must indicate the affected packages in parentheses to "scope" the changes. Changes to the payload chore package do not require scoping.
|
||||
|
||||
Here are some examples:
|
||||
|
||||
- `feat(ui): add new feature`
|
||||
- `fix(richtext-lexical): fix bug`
|
||||
|
||||
If you are committing to [templates](./templates) or [examples](./examples), use the `chore` type with the proper scope, like this:
|
||||
|
||||
- `chore(templates): adds feature to template`
|
||||
- `chore(examples): fixes bug in example`
|
||||
|
||||
## Pull Requests
|
||||
|
||||
For all Pull Requests, you should be extremely descriptive about both your problem and proposed solution. If there are any affected open or closed issues, please leave the issue number in your PR message.
|
||||
|
||||
## Previewing docs
|
||||
|
||||
This is how you can preview changes you made locally to the docs:
|
||||
|
||||
1. Clone our [website repository](https://github.com/payloadcms/website)
|
||||
2. Run `yarn install`
|
||||
2. Run `pnpm install`
|
||||
3. Duplicate the `.env.example` file and rename it to `.env`
|
||||
4. Add a `DOCS_DIR` environment variable to the `.env` file which points to the absolute path of your modified docs folder. For example `DOCS_DIR=/Users/yourname/Documents/GitHub/payload/docs`
|
||||
5. Run `yarn run fetchDocs:local`. If this was successful, you should see no error messages and the following output: _Docs successfully written to /.../website/src/app/docs.json_. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
|
||||
6. You're done! Now you can start the website locally using `yarn run dev` and preview the docs under [http://localhost:3000/docs/](http://localhost:3000/docs/)
|
||||
5. Run `pnpm fetchDocs:local`. If this was successful, you should see no error messages and the following output: _Docs successfully written to /.../website/src/app/docs.json_. There could be error messages if you have incorrect markdown in your local docs folder. In this case, it will tell you how you can fix it
|
||||
6. You're done! Now you can start the website locally using `pnpm dev` and preview the docs under [http://localhost:3000/docs/local](http://localhost:3000/docs/local)
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ const GlobalWithAccessControl: GlobalConfig = {
|
||||
update: ({ req: { user } }) => {...},
|
||||
|
||||
// Version-enabled Globals only
|
||||
readVersion: () => {...},
|
||||
readVersions: () => {...},
|
||||
},
|
||||
// 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 `read` property in the [Global Config](../configuration/globals):
|
||||
To add read Access Control to a [Global](../configuration/globals), use the `access` 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
|
||||
read: {
|
||||
access: {
|
||||
read: ({ req: { user } }) => {
|
||||
return Boolean(user)
|
||||
},
|
||||
|
||||
@@ -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 very 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 vary from framework to framework although the underlying concept is the same.
|
||||
|
||||
### Next.js
|
||||
|
||||
|
||||
@@ -605,7 +605,7 @@ return (
|
||||
textField: {
|
||||
initialValue: 'Updated text',
|
||||
valid: true,
|
||||
value: 'Upddated text',
|
||||
value: 'Updated 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 effects permissions has been changed) |
|
||||
| **`refreshPermissions`** | Load new permissions (useful when content that affects permissions has been changed) |
|
||||
| **`permissions`** | The permissions of the current user |
|
||||
|
||||
```tsx
|
||||
@@ -981,7 +981,15 @@ const MyComponent: React.FC = () => {
|
||||
|
||||
## useTableColumns
|
||||
|
||||
Returns methods to manipulate table columns
|
||||
Returns properties and methods to manipulate table columns:
|
||||
| Property | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------ |
|
||||
| **`columns`** | The current state of columns including their active status and configuration |
|
||||
| **`LinkedCellOverride`** | A component override for linked cells in the table |
|
||||
| **`moveColumn`** | A method to reorder columns. Accepts `{ fromIndex: number, toIndex: number }` as arguments |
|
||||
| **`resetColumnsState`** | A method to reset columns back to their default configuration as defined in the collection config |
|
||||
| **`setActiveColumns`** | A method to set specific columns to active state while preserving the existing column order. Accepts an array of column names to activate |
|
||||
| **`toggleColumn`** | A method to toggle a single column's visibility. Accepts a column name as string |
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
@@ -989,17 +997,30 @@ import { useTableColumns } from '@payloadcms/ui'
|
||||
|
||||
const MyComponent: React.FC = () => {
|
||||
// highlight-start
|
||||
const { setActiveColumns } = useTableColumns()
|
||||
const { setActiveColumns, resetColumnsState } = useTableColumns()
|
||||
|
||||
const resetColumns = () => {
|
||||
setActiveColumns(['id', 'createdAt', 'updatedAt'])
|
||||
const activateSpecificColumns = () => {
|
||||
// Only activates the id and createdAt columns
|
||||
// Other columns retain their current active/inactive state
|
||||
// The original column order is preserved
|
||||
setActiveColumns(['id', 'createdAt'])
|
||||
}
|
||||
|
||||
const resetToDefaults = () => {
|
||||
// Resets to the default columns defined in the collection config
|
||||
resetColumnsState()
|
||||
}
|
||||
// highlight-end
|
||||
|
||||
return (
|
||||
<button type="button" onClick={resetColumns}>
|
||||
Reset columns
|
||||
</button>
|
||||
<div>
|
||||
<button type="button" onClick={activateSpecificColumns}>
|
||||
Activate Specific Columns
|
||||
</button>
|
||||
<button type="button" onClick={resetToDefaults}>
|
||||
Reset To Defaults
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
@@ -1143,7 +1164,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 dy default.
|
||||
By default, any instances of `Link` from `@payloadcms/ui` will trigger route transitions by default.
|
||||
|
||||
```tsx
|
||||
import { Link } from '@payloadcms/ui'
|
||||
|
||||
@@ -62,7 +62,7 @@ In this scenario, if your cookie was still valid, malicious-intent.com would be
|
||||
|
||||
### CSRF Prevention
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
```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 its sent over HTTPS only
|
||||
HttpOnly: true // ensures its not accessible via client side JavaScript
|
||||
Secure: true // ensures it's sent over HTTPS only
|
||||
HttpOnly: true // ensures it's not accessible via client side JavaScript
|
||||
```
|
||||
|
||||
Configuration example:
|
||||
|
||||
@@ -25,11 +25,12 @@ A strategy is made up of the following:
|
||||
|
||||
The `authenticate` function is passed the following arguments:
|
||||
|
||||
| Argument | Description |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| **`headers`** \* | The headers on the incoming request. Useful for retrieving identifiable information on a request. |
|
||||
| **`payload`** \* | The Payload class. Useful for authenticating the identifiable information against Payload. |
|
||||
| **`isGraphQL`** | Whether or not the request was made from a GraphQL endpoint. Default is `false`. |
|
||||
| Argument | Description |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`canSetHeaders`** \* | Whether or not the strategy is being executed from a context where response headers can be set. Default is `false`. |
|
||||
| **`headers`** \* | The headers on the incoming request. Useful for retrieving identifiable information on a request. |
|
||||
| **`payload`** \* | The Payload class. Useful for authenticating the identifiable information against Payload. |
|
||||
| **`isGraphQL`** | Whether or not the strategy is being executed within the GraphQL endpoint. Default is `false`. |
|
||||
|
||||
### Example Strategy
|
||||
|
||||
|
||||
@@ -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 argument 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 arguments 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 argument 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 arguments are the same but you can only return a string - not HTML.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
@@ -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 `removeTokenFromResponse` 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 `removeTokenFromResponses` 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: {
|
||||
removeTokenFromResponse: true, // highlight-line
|
||||
removeTokenFromResponses: true, // highlight-line
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -67,7 +67,7 @@ query {
|
||||
}
|
||||
```
|
||||
|
||||
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`.
|
||||
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`.
|
||||
|
||||
## Me
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ export const Admins: CollectionConfig = {
|
||||
</Banner>
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** Auth-enabled Collections with be automatically injected with the
|
||||
**Note:** Auth-enabled Collections will be automatically injected with the
|
||||
`hash`, `salt`, and `email` fields. [More
|
||||
details](../fields/overview#field-names).
|
||||
</Banner>
|
||||
@@ -142,14 +142,17 @@ import { buildConfig } from 'payload'
|
||||
export default buildConfig({
|
||||
// ...
|
||||
// highlight-start
|
||||
autoLogin:
|
||||
process.env.NEXT_PUBLIC_ENABLE_AUTOLOGIN === 'true'
|
||||
? {
|
||||
email: 'test@example.com',
|
||||
password: 'test',
|
||||
prefillOnly: true,
|
||||
}
|
||||
: false,
|
||||
admin: {
|
||||
autoLogin:
|
||||
process.env.NODE_ENV === 'development'
|
||||
? {
|
||||
email: 'test@example.com',
|
||||
password: 'test',
|
||||
prefillOnly: true,
|
||||
}
|
||||
: false,
|
||||
},
|
||||
|
||||
// highlight-end
|
||||
})
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
### Definining Token Data
|
||||
### Defining Token Data
|
||||
|
||||
You can specify what data gets encoded to the Cookie/JWT-Token by setting `saveToJWT` property on fields within your auth collection.
|
||||
|
||||
|
||||
@@ -132,6 +132,7 @@ 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). |
|
||||
@@ -176,7 +177,7 @@ The following options are available:
|
||||
#### Edit View Options
|
||||
|
||||
```ts
|
||||
import type { CollectionCOnfig } from 'payload'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const MyCollection: CollectionConfig = {
|
||||
// ...
|
||||
@@ -274,7 +275,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 `SanitizeCollectionConfig`.
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -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 `SanitizeGlobalConfig`.
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ 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. |
|
||||
@@ -212,7 +213,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 a 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 an object with the following properties:
|
||||
|
||||
| Option | Description |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -291,7 +292,7 @@ export const script = async (config: SanitizedConfig) => {
|
||||
collection: 'pages',
|
||||
data: { title: 'my title' },
|
||||
})
|
||||
payload.logger.info('Succesffully seeded!')
|
||||
payload.logger.info('Successfully seeded!')
|
||||
process.exit(0)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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](#ocument-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](#document-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.
|
||||
|
||||
|
||||
@@ -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 the `Nav` component. [More details](#graphicsicon). |
|
||||
| `graphics.Icon` | The simplified logo used in contexts like 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.graphic.Logo` property in your Payload Config:
|
||||
To add a custom logo, use the `admin.components.graphics.Logo` property in your Payload Config:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
@@ -183,13 +183,13 @@ Depending on which Database Adapter you use, your migration workflow might diffe
|
||||
|
||||
In relational databases, migrations will be **required** for non-development database environments. But with MongoDB, you might only need to run migrations once in a while (or never even need them).
|
||||
|
||||
#### MongoDB
|
||||
#### MongoDB#mongodb-migrations
|
||||
|
||||
In MongoDB, you'll only ever really need to run migrations for times where you change your database shape, and you have lots of existing data that you'd like to transform from Shape A to Shape B.
|
||||
|
||||
In this case, you can create a migration by running `pnpm payload migrate:create`, and then write the logic that you need to perform to migrate your documents to their new shape. You can then either run your migrations in CI before you build / deploy, or you can run them locally, against your production database, by using your production database connection string on your local computer and running the `pnpm payload migrate` command.
|
||||
|
||||
#### Postgres
|
||||
#### Postgres#postgres-migrations
|
||||
|
||||
In relational databases like Postgres, migrations are a bit more important, because each time you add a new field or a new collection, you'll need to update the shape of your database to match your Payload Config (otherwise you'll see errors upon trying to read / write your data).
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export default buildConfig({
|
||||
import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres'
|
||||
|
||||
export default buildConfig({
|
||||
// Automatically uses proces.env.POSTGRES_URL if no options are provided.
|
||||
// Automatically uses process.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` utillity 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` utility 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
|
||||
|
||||
@@ -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` utillity 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` utility 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
|
||||
|
||||
@@ -80,7 +80,7 @@ export const MyArrayField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
The Array Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
The Array Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Option | Description |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -78,7 +78,7 @@ export const MyBlocksField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
The Blocks Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
The Blocks Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------- | -------------------------------------------------------------------------- |
|
||||
|
||||
@@ -68,7 +68,7 @@ export const MyCodeField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
The Code Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
The Code Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Option | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -58,7 +58,7 @@ export const MyCollapsibleField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
The Collapsible Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
The Collapsible Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Option | Description |
|
||||
| ------------------- | ------------------------------- |
|
||||
|
||||
@@ -65,7 +65,7 @@ export const MyDateField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
The Date Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
The Date Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -65,7 +65,7 @@ export const MyEmailField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
The Email Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
The Email Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------ | ------------------------------------------------------------------------- |
|
||||
|
||||
@@ -37,7 +37,7 @@ export const MyGroupField: Field = {
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`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. Defaults to the field name, if defined. |
|
||||
| **`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 options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
The Group Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Option | Description |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
@@ -113,8 +113,7 @@ 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.
|
||||
You can also use the Group field to only visually group fields without affecting the data structure. Not defining a label will render just the grouped fields.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
@@ -67,7 +67,7 @@ export const MyJSONField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
The JSON Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
The JSON Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Option | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -70,7 +70,7 @@ export const MyNumberField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
The Number Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
The Number Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Property | Description |
|
||||
| ------------------ | --------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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 an `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 a `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 options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
The Radio Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Property | Description |
|
||||
| ------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -86,7 +86,7 @@ export const MyRelationshipField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
The Relationship Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
The Relationship Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Property | Description |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -54,6 +54,7 @@ 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) |
|
||||
|
||||
@@ -67,6 +68,61 @@ _\* 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 current user, document data, or other criteria.
|
||||
|
||||
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:
|
||||
@@ -83,7 +139,7 @@ export const MySelectField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
The Select Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
The Select Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Property | Description |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -70,7 +70,7 @@ export const MyTextField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
The Text Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
The Text Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Option | Description |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -67,7 +67,7 @@ export const MyTextareaField: Field = {
|
||||
}
|
||||
```
|
||||
|
||||
The Textarea Field inherits all of the default options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
The Textarea Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options:
|
||||
|
||||
| Option | Description |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
103
docs/folders/overview.mdx
Normal file
103
docs/folders/overview.mdx
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
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
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
@@ -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 the context in 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 in the context of a later hook.
|
||||
|
||||
For example:
|
||||
|
||||
|
||||
@@ -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 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 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 `ìnlineTask` arguments passed to the task `handler` function:
|
||||
You can run sub-tasks within an existing task, by using the `tasks` or `inlineTask` arguments passed to the task `handler` function:
|
||||
|
||||
```ts
|
||||
export default buildConfig({
|
||||
|
||||
@@ -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 allows requests to be made between the two domains
|
||||
// This will allow requests to be made between the two domains
|
||||
cors: [
|
||||
'http://localhost:3001' // Your front-end application
|
||||
],
|
||||
|
||||
@@ -329,7 +329,7 @@ available:
|
||||
// responseHeaders: { ... } // returned headers from the response
|
||||
// }
|
||||
|
||||
const result = await payload.auth({ headers })
|
||||
const result = await payload.auth({ headers, canSetHeaders: false })
|
||||
```
|
||||
|
||||
### Login
|
||||
|
||||
@@ -85,6 +85,7 @@ formBuilderPlugin({
|
||||
checkbox: true,
|
||||
number: true,
|
||||
message: true,
|
||||
date: false,
|
||||
payment: false,
|
||||
},
|
||||
})
|
||||
@@ -349,6 +350,18 @@ 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.
|
||||
@@ -421,6 +434,42 @@ 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.
|
||||
|
||||
@@ -16,8 +16,8 @@ This plugin sets up multi-tenancy for your application from within your [Admin P
|
||||
If you need help, check out our [Community
|
||||
Help](https://payloadcms.com/community-help). If you think you've found a bug,
|
||||
please [open a new
|
||||
issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%multi-tenant&template=bug_report.md&title=plugin-multi-tenant%3A)
|
||||
with as much detail as possible.
|
||||
issue](https://github.com/payloadcms/payload/issues/new/choose) with as much
|
||||
detail as possible.
|
||||
</Banner>
|
||||
|
||||
## Core features
|
||||
@@ -35,7 +35,7 @@ This plugin sets up multi-tenancy for your application from within your [Admin P
|
||||
By default this plugin cleans up documents when a tenant is deleted. You should ensure you have
|
||||
strong access control on your tenants collection to prevent deletions by unauthorized users.
|
||||
|
||||
You can disabled this behavior by setting `cleanupAfterTenantDelete` to `false` in the plugin options.
|
||||
You can disable this behavior by setting `cleanupAfterTenantDelete` to `false` in the plugin options.
|
||||
|
||||
</Banner>
|
||||
|
||||
|
||||
32
docs/production/building-without-a-db-connection.mdx
Normal file
32
docs/production/building-without-a-db-connection.mdx
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Building without a DB connection
|
||||
label: Building without a DB connection
|
||||
order: 10
|
||||
desc: You don't want to have a DB connection while building your Docker container? Learn how to prevent that!
|
||||
keywords: deployment, production, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
# Building without a DB connection
|
||||
|
||||
One of the most common problems when building a site for production, especially with Docker - is the DB connection requirement.
|
||||
|
||||
The important note is that Payload by itself does not have this requirement, But [Next.js' SSG ](https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation) does if any of your route segments have SSG enabled (which is default, unless you opted out or used a [Dynamic API](https://nextjs.org/docs/app/deep-dive/caching#dynamic-apis)) and use the Payload Local API.
|
||||
|
||||
Solutions:
|
||||
|
||||
## Using the experimental-build-mode Next.js build flag
|
||||
|
||||
You can run Next.js build using the `pnpx next build --experimental-build-mode compile` command to only compile the code without static generation, which does not require a DB connection. In that case, your pages will be rendered dynamically, but after that, you can still generate static pages using the `pnpx next build --experimental-build-mode generate` command when you have a DB connection.
|
||||
|
||||
[Next.js documentation](https://nextjs.org/docs/pages/api-reference/cli/next#next-build-options)
|
||||
|
||||
## Opting-out of SSG
|
||||
|
||||
You can opt out of SSG by adding this all the route segment files:
|
||||
|
||||
```ts
|
||||
export const dynamic = 'force-dynamic'
|
||||
```
|
||||
|
||||
**Note that it will disable static optimization and your site will be slower**.
|
||||
More on [Next.js documentation](https://nextjs.org/docs/app/deep-dive/caching#opting-out-2)
|
||||
@@ -150,7 +150,7 @@ Follow the docs to configure any one of these storage providers. For local devel
|
||||
## Docker
|
||||
|
||||
This is an example of a multi-stage docker build of Payload for production. Ensure you are setting your environment
|
||||
variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed.
|
||||
variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed. If you don't want to have a DB connection and your build requires that, learn [here](./building-without-a-db-connection) how to prevent that.
|
||||
|
||||
In your Next.js config, set the `output` property `standalone`.
|
||||
|
||||
|
||||
@@ -46,11 +46,12 @@ const config = buildConfig({
|
||||
|
||||
The following options are available for Query Presets:
|
||||
|
||||
| Option | Description |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `access` | Used to define custom collection-level access control that applies to all presets. [More details](#access-control). |
|
||||
| `constraints` | Used to define custom document-level access control that apply to individual presets. [More details](#document-access-control). |
|
||||
| `labels` | Custom labels to use for the Query Presets collection. |
|
||||
| Option | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `access` | Used to define custom collection-level access control that applies to all presets. [More details](#access-control). |
|
||||
| `filterConstraints` | Used to define which constraints are available to users when managing presets. [More details](#constraint-access-control). |
|
||||
| `constraints` | Used to define custom document-level access control that apply to individual presets. [More details](#document-access-control). |
|
||||
| `labels` | Custom labels to use for the Query Presets collection. |
|
||||
|
||||
## Access Control
|
||||
|
||||
@@ -59,7 +60,7 @@ Query Presets are subject to the same [Access Control](../access-control/overvie
|
||||
Access Control for Query Presets can be customized in two ways:
|
||||
|
||||
1. [Collection Access Control](#collection-access-control): Applies to all presets. These rules are not controllable by the user and are statically defined in the config.
|
||||
2. [Document Access Control](#document-access-control): Applies to each individual preset. These rules are controllable by the user and are saved to the document.
|
||||
2. [Document Access Control](#document-access-control): Applies to each individual preset. These rules are controllable by the user and are dynamically defined on each record in the database.
|
||||
|
||||
### Collection Access Control
|
||||
|
||||
@@ -97,7 +98,7 @@ This example restricts all Query Presets to users with the role of `admin`.
|
||||
|
||||
### Document Access Control
|
||||
|
||||
You can also define access control rules that apply to each specific preset. Users have the ability to define and modify these rules on the fly as they manage presets. These are saved dynamically in the database on each document.
|
||||
You can also define access control rules that apply to each specific preset. Users have the ability to define and modify these rules on the fly as they manage presets. These are saved dynamically in the database on each record.
|
||||
|
||||
When a user manages a preset, document-level access control options will be available to them in the Admin Panel for each operation.
|
||||
|
||||
@@ -150,8 +151,8 @@ const config = buildConfig({
|
||||
}),
|
||||
},
|
||||
],
|
||||
// highlight-end
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -171,3 +172,39 @@ The following options are available for each constraint:
|
||||
| `value` | The value to store in the database when this constraint is selected. |
|
||||
| `fields` | An array of fields to render when this constraint is selected. |
|
||||
| `access` | A function that determines the access control rules for this constraint. |
|
||||
|
||||
### Constraint Access Control
|
||||
|
||||
Used to dynamically filter which constraints are available based on the current user, document data, or other criteria.
|
||||
|
||||
Some examples of this might include:
|
||||
|
||||
- Ensuring that only "admins" are allowed to make a preset available to "everyone"
|
||||
- Preventing the "onlyMe" option from being selected based on a hypothetical "disablePrivatePresets" checkbox
|
||||
|
||||
When a user lacks the permission to set a constraint, the option will either be hidden from them, or disabled if it is already saved to that preset.
|
||||
|
||||
To do this, you can use the `filterConstraints` property in your [Payload Config](../configuration/overview):
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
queryPresets: {
|
||||
// ...
|
||||
// highlight-start
|
||||
filterConstraints: ({ req, options }) =>
|
||||
!req.user?.roles?.includes('admin')
|
||||
? options.filter(
|
||||
(option) =>
|
||||
(typeof option === 'string' ? option : option.value) !==
|
||||
'everyone',
|
||||
)
|
||||
: options,
|
||||
// highlight-end
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The `filterConstraints` function receives the same arguments as [`filterOptions`](../fields/select#filterOptions) in the [Select field](../fields/select).
|
||||
|
||||
@@ -738,7 +738,7 @@ Payload supports a method override feature that allows you to send GET requests
|
||||
|
||||
### How to Use
|
||||
|
||||
To use this feature, include the `X-HTTP-Method-Override` header set to `GET` in your POST request. The parameters should be sent in the body of the request with the `Content-Type` set to `application/x-www-form-urlencoded`.
|
||||
To use this feature, include the `X-Payload-HTTP-Method-Override` header set to `GET` in your POST request. The parameters should be sent in the body of the request with the `Content-Type` set to `application/x-www-form-urlencoded`.
|
||||
|
||||
### Example
|
||||
|
||||
@@ -753,7 +753,7 @@ const res = await fetch(`${api}/${collectionSlug}`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-HTTP-Method-Override': 'GET',
|
||||
'X-Payload-HTTP-Method-Override': 'GET',
|
||||
},
|
||||
body: qs.stringify({
|
||||
depth: 1,
|
||||
|
||||
@@ -6,14 +6,14 @@ desc: Converting between lexical richtext and HTML
|
||||
keywords: lexical, richtext, html
|
||||
---
|
||||
|
||||
## Converting Rich Text to HTML
|
||||
## 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.
|
||||
|
||||
### Generating HTML on-demand (Recommended)
|
||||
### On-demand
|
||||
|
||||
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,61 +32,81 @@ export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
|
||||
}
|
||||
```
|
||||
|
||||
### Converting Lexical Blocks
|
||||
#### 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 }} />
|
||||
}
|
||||
```
|
||||
|
||||
### Outputting HTML from the Collection
|
||||
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:
|
||||
|
||||
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:
|
||||
```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 }} />
|
||||
}
|
||||
```
|
||||
|
||||
### HTML field
|
||||
|
||||
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.
|
||||
|
||||
```ts
|
||||
import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html'
|
||||
@@ -154,74 +174,59 @@ const Pages: CollectionConfig = {
|
||||
}
|
||||
```
|
||||
|
||||
### Generating HTML in Your Frontend with Dynamic Population (Advanced)
|
||||
## Blocks to HTML
|
||||
|
||||
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 }} />
|
||||
}
|
||||
```
|
||||
|
||||
## Converting HTML to Richtext
|
||||
## 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):
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and JSX
|
||||
keywords: lexical, richtext, jsx
|
||||
---
|
||||
|
||||
## Converting Richtext to JSX
|
||||
## 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>
|
||||
|
||||
### Converting Internal Links
|
||||
### 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<{
|
||||
}
|
||||
```
|
||||
|
||||
### Converting Lexical Blocks
|
||||
### 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 Default JSX Converters
|
||||
### Overriding Converters
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Example - overriding the upload node converter to use next/image:
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and Markdown / MDX
|
||||
keywords: lexical, richtext, markdown, md, mdx
|
||||
---
|
||||
|
||||
## Converting Richtext to Markdown
|
||||
## 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 = {
|
||||
}
|
||||
```
|
||||
|
||||
## Converting Markdown to Richtext
|
||||
## 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:
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Converting between lexical richtext and plaintext
|
||||
keywords: lexical, richtext, plaintext, text
|
||||
---
|
||||
|
||||
## Converting Richtext to Plaintext
|
||||
## Richtext to Plaintext
|
||||
|
||||
Here's how you can convert richtext data to plaintext using `@payloadcms/richtext-lexical/plaintext`.
|
||||
|
||||
|
||||
@@ -142,32 +142,33 @@ 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. |
|
||||
| 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. |
|
||||
|
||||
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!
|
||||
|
||||
|
||||
@@ -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, nextjss
|
||||
keywords: version history, revisions, audit log, draft, publish, autosave, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
@@ -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,6 +25,7 @@ export const Users: CollectionConfig = {
|
||||
create: anyone,
|
||||
update: adminsAndUser,
|
||||
delete: admins,
|
||||
unlock: admins,
|
||||
admin: ({ req: { user } }) => checkRole(['admin'], user),
|
||||
},
|
||||
hooks: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Access } from 'payload/config'
|
||||
import type { Access } from 'payload'
|
||||
|
||||
import { checkRole } from './checkRole'
|
||||
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import type { Access } from 'payload/config'
|
||||
import type { Access } from 'payload'
|
||||
|
||||
import { checkRole } from './checkRole'
|
||||
|
||||
const adminsAndUser: Access = ({ req: { user } }) => {
|
||||
export const adminsAndUser: Access = ({ req: { user } }) => {
|
||||
if (user) {
|
||||
if (checkRole(['admin'], user)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
id: { equals: user.id },
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export default adminsAndUser
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import type { Access } from 'payload/config'
|
||||
import type { Access } from 'payload'
|
||||
|
||||
export const anyone: Access = () => true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { User } from '../../payload-types'
|
||||
|
||||
export const checkRole = (allRoles: User['roles'] = [], user: User = undefined): boolean => {
|
||||
export const checkRole = (allRoles: User['roles'] = [], user: User | null = null): boolean => {
|
||||
if (user) {
|
||||
if (
|
||||
allRoles.some((role) => {
|
||||
@@ -8,8 +8,9 @@ export const checkRole = (allRoles: User['roles'] = [], user: User = undefined):
|
||||
return individualRole === role
|
||||
})
|
||||
})
|
||||
)
|
||||
{return true}
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FieldHook } from 'payload/types'
|
||||
import type { FieldHook } from 'payload'
|
||||
|
||||
import type { User } from '../../payload-types'
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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'
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import express from 'express'
|
||||
import type { Request, Response } from 'express'
|
||||
import { parse } from 'url'
|
||||
import next from 'next'
|
||||
|
||||
|
||||
@@ -3,14 +3,8 @@
|
||||
width: var(--base);
|
||||
|
||||
.stroke {
|
||||
stroke-width: 1px;
|
||||
stroke-width: 2px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
&:local() {
|
||||
.stroke {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
examples/live-preview/next-env.d.ts
vendored
2
examples/live-preview/next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Payload Live Preview example.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "dist/server.js",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_OPTIONS=--no-deprecation next build",
|
||||
|
||||
3278
examples/live-preview/pnpm-lock.yaml
generated
3278
examples/live-preview/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
import type { MigrateUpArgs } from '@payloadcms/db-mongodb'
|
||||
|
||||
import type { Page } from '../payload-types'
|
||||
import { DefaultDocumentIDType } from 'payload'
|
||||
|
||||
export const home: Partial<Page> = {
|
||||
export const home = (id: DefaultDocumentIDType): Partial<Page> => ({
|
||||
slug: 'home',
|
||||
richText: [
|
||||
{
|
||||
@@ -41,11 +42,26 @@ export const home: Partial<Page> = {
|
||||
{
|
||||
text: ' you can edit this page in the admin panel and see the changes reflected here in real time.',
|
||||
},
|
||||
...(id
|
||||
? [
|
||||
{
|
||||
text: ' To get started, visit ',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
children: [{ text: 'this page' }],
|
||||
linkType: 'custom',
|
||||
newTab: true,
|
||||
url: `/admin/collections/pages/${id}/preview`,
|
||||
},
|
||||
{ text: '.' },
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
],
|
||||
title: 'Home',
|
||||
}
|
||||
})
|
||||
|
||||
export const examplePage: Partial<Page> = {
|
||||
slug: 'example-page',
|
||||
@@ -83,11 +99,18 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
data: examplePage as any, // eslint-disable-line
|
||||
})
|
||||
|
||||
const homepageJSON = JSON.parse(JSON.stringify(home))
|
||||
|
||||
const { id: homePageID } = await payload.create({
|
||||
const { id: ogHomePageID } = await payload.create({
|
||||
collection: 'pages',
|
||||
data: homepageJSON,
|
||||
data: {
|
||||
title: 'Home',
|
||||
richText: [],
|
||||
},
|
||||
})
|
||||
|
||||
const { id: homePageID } = await payload.update({
|
||||
id: ogHomePageID,
|
||||
collection: 'pages',
|
||||
data: home(ogHomePageID),
|
||||
})
|
||||
|
||||
await payload.updateGlobal({
|
||||
@@ -121,7 +144,7 @@ export async function up({ payload }: MigrateUpArgs): Promise<void> {
|
||||
type: 'custom',
|
||||
label: 'Dashboard',
|
||||
reference: undefined,
|
||||
url: 'http://localhost:3000/admin',
|
||||
url: '/admin',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -6,10 +6,66 @@
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| 'Pacific/Midway'
|
||||
| 'Pacific/Niue'
|
||||
| 'Pacific/Honolulu'
|
||||
| 'Pacific/Rarotonga'
|
||||
| 'America/Anchorage'
|
||||
| 'Pacific/Gambier'
|
||||
| 'America/Los_Angeles'
|
||||
| 'America/Tijuana'
|
||||
| 'America/Denver'
|
||||
| 'America/Phoenix'
|
||||
| 'America/Chicago'
|
||||
| 'America/Guatemala'
|
||||
| 'America/New_York'
|
||||
| 'America/Bogota'
|
||||
| 'America/Caracas'
|
||||
| 'America/Santiago'
|
||||
| 'America/Buenos_Aires'
|
||||
| 'America/Sao_Paulo'
|
||||
| 'Atlantic/South_Georgia'
|
||||
| 'Atlantic/Azores'
|
||||
| 'Atlantic/Cape_Verde'
|
||||
| 'Europe/London'
|
||||
| 'Europe/Berlin'
|
||||
| 'Africa/Lagos'
|
||||
| 'Europe/Athens'
|
||||
| 'Africa/Cairo'
|
||||
| 'Europe/Moscow'
|
||||
| 'Asia/Riyadh'
|
||||
| 'Asia/Dubai'
|
||||
| 'Asia/Baku'
|
||||
| 'Asia/Karachi'
|
||||
| 'Asia/Tashkent'
|
||||
| 'Asia/Calcutta'
|
||||
| 'Asia/Dhaka'
|
||||
| 'Asia/Almaty'
|
||||
| 'Asia/Jakarta'
|
||||
| 'Asia/Bangkok'
|
||||
| 'Asia/Shanghai'
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
| 'Pacific/Auckland'
|
||||
| 'Pacific/Fiji';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
pages: Page;
|
||||
users: User;
|
||||
|
||||
35
package.json
35
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.38.0",
|
||||
"version": "3.40.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -84,7 +84,7 @@
|
||||
"publish-prerelease": "pnpm --filter releaser publish-prerelease",
|
||||
"reinstall": "pnpm clean:all && pnpm install",
|
||||
"release": "pnpm --filter releaser release --tag latest",
|
||||
"runts": "cross-env NODE_OPTIONS=--no-deprecation node --no-deprecation --import @swc-node/register/esm-register",
|
||||
"runts": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" node --no-deprecation --no-experimental-strip-types --import @swc-node/register/esm-register",
|
||||
"script:build-template-with-local-pkgs": "pnpm --filter scripts build-template-with-local-pkgs",
|
||||
"script:gen-templates": "pnpm --filter scripts gen-templates",
|
||||
"script:gen-templates:build": "pnpm --filter scripts gen-templates --build",
|
||||
@@ -93,17 +93,19 @@
|
||||
"script:pack": "pnpm --filter scripts pack-all-to-dest",
|
||||
"pretest": "pnpm build",
|
||||
"test": "pnpm test:int && pnpm test:components && pnpm test:e2e",
|
||||
"test:components": "cross-env NODE_OPTIONS=\" --no-deprecation\" jest --config=jest.components.config.js",
|
||||
"test:components": "cross-env NODE_OPTIONS=\" --no-deprecation --no-experimental-strip-types\" jest --config=jest.components.config.js",
|
||||
"test:e2e": "pnpm runts ./test/runE2E.ts",
|
||||
"test:e2e:debug": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 PWDEBUG=1 DISABLE_LOGGING=true playwright test",
|
||||
"test:e2e:headed": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 DISABLE_LOGGING=true playwright test --headed",
|
||||
"test:e2e:debug": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PWDEBUG=1 DISABLE_LOGGING=true playwright test",
|
||||
"test:e2e:headed": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true playwright test --headed",
|
||||
"test:e2e:prod": "pnpm prepare-run-test-against-prod && pnpm runts ./test/runE2E.ts --prod",
|
||||
"test:e2e:prod:ci": "pnpm prepare-run-test-against-prod:ci && pnpm runts ./test/runE2E.ts --prod",
|
||||
"test:int": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:int:postgres": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:int:sqlite": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=sqlite DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:e2e:prod:ci:turbo": "pnpm prepare-run-test-against-prod:ci && pnpm runts ./test/runE2E.ts --prod --turbo",
|
||||
"test:e2e:turbo": "pnpm runts ./test/runE2E.ts --turbo",
|
||||
"test:int": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:int:postgres": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:int:sqlite": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=sqlite DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
|
||||
"test:types": "tstyche",
|
||||
"test:unit": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
|
||||
"test:unit": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
|
||||
"translateNewKeys": "pnpm --filter translations run translateNewKeys"
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -120,7 +122,7 @@
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@libsql/client": "0.14.0",
|
||||
"@next/bundle-analyzer": "15.3.0",
|
||||
"@next/bundle-analyzer": "15.3.2",
|
||||
"@payloadcms/db-postgres": "workspace:*",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/eslint-plugin": "workspace:*",
|
||||
@@ -128,9 +130,9 @@
|
||||
"@playwright/test": "1.50.0",
|
||||
"@sentry/nextjs": "^8.33.1",
|
||||
"@sentry/node": "^8.33.1",
|
||||
"@swc-node/register": "1.10.9",
|
||||
"@swc/cli": "0.6.0",
|
||||
"@swc/jest": "0.2.37",
|
||||
"@swc-node/register": "1.10.10",
|
||||
"@swc/cli": "0.7.7",
|
||||
"@swc/jest": "0.2.38",
|
||||
"@types/fs-extra": "^11.0.2",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/minimist": "1.2.5",
|
||||
@@ -145,8 +147,6 @@
|
||||
"cross-env": "7.0.3",
|
||||
"dotenv": "16.4.7",
|
||||
"drizzle-kit": "0.28.0",
|
||||
"drizzle-orm": "0.36.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"execa": "5.1.1",
|
||||
"form-data": "3.0.1",
|
||||
"fs-extra": "10.1.0",
|
||||
@@ -156,7 +156,7 @@
|
||||
"lint-staged": "15.2.7",
|
||||
"minimist": "1.2.8",
|
||||
"mongodb-memory-server": "^10",
|
||||
"next": "15.3.0",
|
||||
"next": "15.3.2",
|
||||
"open": "^10.1.0",
|
||||
"p-limit": "^5.0.0",
|
||||
"playwright": "1.50.0",
|
||||
@@ -169,7 +169,7 @@
|
||||
"shelljs": "0.8.5",
|
||||
"slash": "3.0.0",
|
||||
"sort-package-json": "^2.10.0",
|
||||
"swc-plugin-transform-remove-imports": "3.1.0",
|
||||
"swc-plugin-transform-remove-imports": "4.0.4",
|
||||
"tempy": "1.0.1",
|
||||
"tstyche": "^3.1.1",
|
||||
"tsx": "4.19.2",
|
||||
@@ -186,7 +186,6 @@
|
||||
"copyfiles": "$copyfiles",
|
||||
"cross-env": "$cross-env",
|
||||
"dotenv": "$dotenv",
|
||||
"drizzle-orm": "$drizzle-orm",
|
||||
"graphql": "^16.8.1",
|
||||
"mongodb-memory-server": "$mongodb-memory-server",
|
||||
"react": "$react",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.38.0",
|
||||
"version": "3.40.0",
|
||||
"description": "An admin bar for React apps using Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.38.0",
|
||||
"version": "3.40.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -16,6 +16,7 @@
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./types": {
|
||||
@@ -60,7 +61,7 @@
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@sindresorhus/slugify": "^1.1.0",
|
||||
"@swc/core": "1.10.12",
|
||||
"@swc/core": "1.11.29",
|
||||
"arg": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"comment-json": "^4.2.3",
|
||||
|
||||
@@ -16,12 +16,15 @@ export const configurePluginProject = ({
|
||||
const devPayloadConfigPath = path.resolve(projectDirPath, './dev/payload.config.ts')
|
||||
const devTsConfigPath = path.resolve(projectDirPath, './dev/tsconfig.json')
|
||||
const indexTsPath = path.resolve(projectDirPath, './src/index.ts')
|
||||
const devImportMapPath = path.resolve(projectDirPath, './dev/app/(payload)/admin/importMap.js')
|
||||
|
||||
const devPayloadConfig = fse.readFileSync(devPayloadConfigPath, 'utf8')
|
||||
const devTsConfig = fse.readFileSync(devTsConfigPath, 'utf8')
|
||||
const indexTs = fse.readFileSync(indexTsPath, 'utf-8')
|
||||
const devImportMap = fse.readFileSync(devImportMapPath, 'utf-8')
|
||||
|
||||
const updatedTsConfig = devTsConfig.replaceAll('plugin-package-name-placeholder', projectName)
|
||||
const updatedImportMap = devImportMap.replaceAll('plugin-package-name-placeholder', projectName)
|
||||
let updatedIndexTs = indexTs.replaceAll('plugin-package-name-placeholder', projectName)
|
||||
|
||||
const pluginExportVariableName = toCamelCase(projectName)
|
||||
@@ -43,4 +46,5 @@ export const configurePluginProject = ({
|
||||
fse.writeFileSync(devPayloadConfigPath, updatedPayloadConfig)
|
||||
fse.writeFileSync(devTsConfigPath, updatedTsConfig)
|
||||
fse.writeFileSync(indexTsPath, updatedIndexTs)
|
||||
fse.writeFileSync(devImportMapPath, updatedImportMap)
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
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
|
||||
@@ -10,10 +10,10 @@ 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
|
||||
|
||||
beforeAll(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log = jest.fn()
|
||||
@@ -63,6 +63,30 @@ describe('createProject', () => {
|
||||
expect(packageJson.name).toStrictEqual(projectName)
|
||||
})
|
||||
|
||||
it('updates project name in plugin template importMap file', async () => {
|
||||
const projectName = 'my-custom-plugin'
|
||||
const template: ProjectTemplate = {
|
||||
name: 'plugin',
|
||||
type: 'plugin',
|
||||
description: 'Template for creating a Payload plugin',
|
||||
url: 'https://github.com/payloadcms/payload/templates/plugin',
|
||||
}
|
||||
|
||||
await createProject({
|
||||
cliArgs: { ...args, '--local-template': 'plugin' } as CliArgs,
|
||||
packageManager,
|
||||
projectDir,
|
||||
projectName,
|
||||
template,
|
||||
})
|
||||
|
||||
const importMapPath = path.resolve(projectDir, './dev/app/(payload)/admin/importMap.js')
|
||||
const importMapFile = fse.readFileSync(importMapPath, 'utf-8')
|
||||
|
||||
expect(importMapFile).not.toContain('plugin-package-name-placeholder')
|
||||
expect(importMapFile).toContain('my-custom-plugin')
|
||||
})
|
||||
|
||||
it('creates example', async () => {
|
||||
const projectName = 'custom-server-example'
|
||||
const example: ProjectExample = {
|
||||
@@ -155,75 +179,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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -144,17 +144,14 @@ export async function createProject(
|
||||
}
|
||||
}
|
||||
|
||||
// Call manageEnvFiles before initializing Git
|
||||
if (dbDetails) {
|
||||
await manageEnvFiles({
|
||||
cliArgs,
|
||||
databaseType: dbDetails.type,
|
||||
databaseUri: dbDetails.dbUri,
|
||||
payloadSecret: generateSecret(),
|
||||
projectDir,
|
||||
template: 'template' in args ? args.template : undefined,
|
||||
})
|
||||
}
|
||||
await manageEnvFiles({
|
||||
cliArgs,
|
||||
databaseType: dbDetails?.type,
|
||||
databaseUri: dbDetails?.dbUri,
|
||||
payloadSecret: generateSecret(),
|
||||
projectDir,
|
||||
template: 'template' in args ? args.template : undefined,
|
||||
})
|
||||
|
||||
// Remove yarn.lock file. This is only desired in Payload Cloud.
|
||||
const lockPath = path.resolve(projectDir, 'pnpm-lock.yaml')
|
||||
|
||||
165
packages/create-payload-app/src/lib/manage-env-files.spec.ts
Normal file
165
packages/create-payload-app/src/lib/manage-env-files.spec.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import fs from 'fs'
|
||||
import fse from 'fs-extra'
|
||||
import * as os from 'node:os'
|
||||
import path from 'path'
|
||||
|
||||
import type { CliArgs } from '../types.js'
|
||||
|
||||
import { manageEnvFiles } from './manage-env-files.js'
|
||||
|
||||
describe('createProject', () => {
|
||||
let projectDir: string
|
||||
let envFilePath = ''
|
||||
let envExampleFilePath = ''
|
||||
|
||||
beforeAll(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log = jest.fn()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
const tempDirectory = fs.realpathSync(os.tmpdir())
|
||||
projectDir = `${tempDirectory}/${Math.random().toString(36).substring(7)}`
|
||||
|
||||
envFilePath = path.join(projectDir, '.env')
|
||||
envExampleFilePath = path.join(projectDir, '.env.example')
|
||||
|
||||
if (fse.existsSync(envFilePath)) {
|
||||
fse.removeSync(envFilePath)
|
||||
}
|
||||
|
||||
fse.ensureDirSync(projectDir)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (fse.existsSync(projectDir)) {
|
||||
fse.rmSync(projectDir, { recursive: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('generates .env using defaults (not from .env.example)', async () => {
|
||||
// ensure no .env.example exists so that the default values are used
|
||||
// the `manageEnvFiles` function will look for .env.example in the file system
|
||||
if (fse.existsSync(envExampleFilePath)) {
|
||||
fse.removeSync(envExampleFilePath)
|
||||
}
|
||||
|
||||
await manageEnvFiles({
|
||||
cliArgs: {
|
||||
'--debug': true,
|
||||
} as CliArgs,
|
||||
databaseUri: '', // omitting this will ensure the default vars are used
|
||||
payloadSecret: '', // omitting this will ensure the default vars are used
|
||||
projectDir,
|
||||
template: undefined,
|
||||
})
|
||||
|
||||
expect(fse.existsSync(envFilePath)).toBe(true)
|
||||
|
||||
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
|
||||
|
||||
expect(updatedEnvContent).toBe(
|
||||
`# Added by Payload\nPAYLOAD_SECRET=YOUR_SECRET_HERE\nDATABASE_URI=your-connection-string-here`,
|
||||
)
|
||||
})
|
||||
|
||||
it('generates .env from .env.example', async () => {
|
||||
// create or override the .env.example file with a connection string that will NOT be overridden
|
||||
fse.ensureFileSync(envExampleFilePath)
|
||||
fse.writeFileSync(
|
||||
envExampleFilePath,
|
||||
`DATABASE_URI=example-connection-string\nCUSTOM_VAR=custom-value\n`,
|
||||
)
|
||||
|
||||
await manageEnvFiles({
|
||||
cliArgs: {
|
||||
'--debug': true,
|
||||
} as CliArgs,
|
||||
databaseUri: '', // omitting this will ensure the `.env.example` vars are used
|
||||
payloadSecret: '', // omitting this will ensure the `.env.example` vars are used
|
||||
projectDir,
|
||||
template: undefined,
|
||||
})
|
||||
|
||||
expect(fse.existsSync(envFilePath)).toBe(true)
|
||||
|
||||
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
|
||||
|
||||
expect(updatedEnvContent).toBe(
|
||||
`DATABASE_URI=example-connection-string\nCUSTOM_VAR=custom-value\nPAYLOAD_SECRET=YOUR_SECRET_HERE\n# Added by Payload`,
|
||||
)
|
||||
})
|
||||
|
||||
it('updates existing .env without overriding vars', async () => {
|
||||
// create an existing .env file with some custom variables that should NOT be overridden
|
||||
fse.ensureFileSync(envFilePath)
|
||||
fse.writeFileSync(
|
||||
envFilePath,
|
||||
`CUSTOM_VAR=custom-value\nDATABASE_URI=example-connection-string\n`,
|
||||
)
|
||||
|
||||
// create an .env.example file to ensure that its contents DO NOT override existing .env vars
|
||||
fse.ensureFileSync(envExampleFilePath)
|
||||
fse.writeFileSync(
|
||||
envExampleFilePath,
|
||||
`CUSTOM_VAR=custom-value-2\nDATABASE_URI=example-connection-string-2\n`,
|
||||
)
|
||||
|
||||
await manageEnvFiles({
|
||||
cliArgs: {
|
||||
'--debug': true,
|
||||
} as CliArgs,
|
||||
databaseUri: '', // omitting this will ensure the `.env` vars are kept
|
||||
payloadSecret: '', // omitting this will ensure the `.env` vars are kept
|
||||
projectDir,
|
||||
template: undefined,
|
||||
})
|
||||
|
||||
expect(fse.existsSync(envFilePath)).toBe(true)
|
||||
|
||||
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
|
||||
|
||||
expect(updatedEnvContent).toBe(
|
||||
`# Added by Payload\nPAYLOAD_SECRET=YOUR_SECRET_HERE\nDATABASE_URI=example-connection-string\nCUSTOM_VAR=custom-value`,
|
||||
)
|
||||
})
|
||||
|
||||
it('sanitizes .env based on selected database type', async () => {
|
||||
await manageEnvFiles({
|
||||
cliArgs: {
|
||||
'--debug': true,
|
||||
} as CliArgs,
|
||||
databaseType: 'mongodb', // this mimics the CLI selection and will be used as the DATABASE_URI
|
||||
databaseUri: 'mongodb://localhost:27017/test', // this mimics the CLI selection and will be used as the DATABASE_URI
|
||||
payloadSecret: 'test-secret', // this mimics the CLI selection and will be used as the PAYLOAD_SECRET
|
||||
projectDir,
|
||||
template: undefined,
|
||||
})
|
||||
|
||||
const updatedEnvContent = fse.readFileSync(envFilePath, 'utf-8')
|
||||
|
||||
expect(updatedEnvContent).toBe(
|
||||
`# Added by Payload\nPAYLOAD_SECRET=test-secret\nDATABASE_URI=mongodb://localhost:27017/test`,
|
||||
)
|
||||
|
||||
// delete the generated .env file and do it again, but this time, omit the databaseUri to ensure the default is generated
|
||||
fse.removeSync(envFilePath)
|
||||
|
||||
await manageEnvFiles({
|
||||
cliArgs: {
|
||||
'--debug': true,
|
||||
} as CliArgs,
|
||||
databaseType: 'mongodb', // this mimics the CLI selection and will be used as the DATABASE_URI
|
||||
databaseUri: '', // omit this to ensure the default is generated based on the selected database type
|
||||
payloadSecret: 'test-secret',
|
||||
projectDir,
|
||||
template: undefined,
|
||||
})
|
||||
|
||||
const updatedEnvContentWithDefault = fse.readFileSync(envFilePath, 'utf-8')
|
||||
expect(updatedEnvContentWithDefault).toBe(
|
||||
`# Added by Payload\nPAYLOAD_SECRET=test-secret\nDATABASE_URI=mongodb://127.0.0.1/your-database-name`,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -6,21 +6,42 @@ 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 sanitizeEnv = ({
|
||||
contents,
|
||||
databaseType,
|
||||
databaseUri,
|
||||
payloadSecret,
|
||||
}: {
|
||||
contents: string
|
||||
databaseType: DbType | undefined
|
||||
databaseUri?: string
|
||||
payloadSecret?: string
|
||||
}): string => {
|
||||
const seenKeys = new Set<string>()
|
||||
const updatedEnv = contents
|
||||
|
||||
// add defaults
|
||||
let withDefaults = contents
|
||||
|
||||
if (
|
||||
!contents.includes('DATABASE_URI') &&
|
||||
!contents.includes('POSTGRES_URL') &&
|
||||
!contents.includes('MONGODB_URI')
|
||||
) {
|
||||
withDefaults += '\nDATABASE_URI=your-connection-string-here'
|
||||
}
|
||||
|
||||
if (!contents.includes('PAYLOAD_SECRET')) {
|
||||
withDefaults += '\nPAYLOAD_SECRET=YOUR_SECRET_HERE'
|
||||
}
|
||||
|
||||
let updatedEnv = withDefaults
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (line.startsWith('#') || !line.includes('=')) {
|
||||
return line
|
||||
}
|
||||
|
||||
const [key] = line.split('=')
|
||||
const [key, value] = line.split('=')
|
||||
|
||||
if (!key) {
|
||||
return
|
||||
@@ -28,6 +49,7 @@ const updateEnvExampleVariables = (
|
||||
|
||||
if (key === 'DATABASE_URI' || key === 'POSTGRES_URL' || key === 'MONGODB_URI') {
|
||||
const dbChoice = databaseType ? dbChoiceRecord[databaseType] : null
|
||||
|
||||
if (dbChoice) {
|
||||
const placeholderUri = databaseUri
|
||||
? databaseUri
|
||||
@@ -36,6 +58,8 @@ const updateEnvExampleVariables = (
|
||||
databaseType === 'vercel-postgres'
|
||||
? `POSTGRES_URL=${placeholderUri}`
|
||||
: `DATABASE_URI=${placeholderUri}`
|
||||
} else {
|
||||
line = `${key}=${value}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +80,10 @@ const updateEnvExampleVariables = (
|
||||
.reverse()
|
||||
.join('\n')
|
||||
|
||||
if (!updatedEnv.includes('# Added by Payload')) {
|
||||
updatedEnv = `# Added by Payload\n${updatedEnv}`
|
||||
}
|
||||
|
||||
return updatedEnv
|
||||
}
|
||||
|
||||
@@ -63,7 +91,7 @@ const updateEnvExampleVariables = (
|
||||
export async function manageEnvFiles(args: {
|
||||
cliArgs: CliArgs
|
||||
databaseType?: DbType
|
||||
databaseUri: string
|
||||
databaseUri?: string
|
||||
payloadSecret: string
|
||||
projectDir: string
|
||||
template?: ProjectTemplate
|
||||
@@ -77,70 +105,63 @@ export async function manageEnvFiles(args: {
|
||||
return
|
||||
}
|
||||
|
||||
const envExamplePath = path.join(projectDir, '.env.example')
|
||||
const pathToEnvExample = 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
|
||||
|
||||
let exampleEnv: null | string = ''
|
||||
|
||||
try {
|
||||
if (template?.type === 'plugin') {
|
||||
if (debugFlag) {
|
||||
debug(`plugin template detected - no .env added .env.example added`)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!fs.existsSync(envExamplePath)) {
|
||||
updatedExampleContents = updateEnvExampleVariables(
|
||||
emptyEnvContent,
|
||||
databaseType,
|
||||
payloadSecret,
|
||||
databaseUri,
|
||||
)
|
||||
// If there's a .env.example file, use it to create or update the .env file
|
||||
if (fs.existsSync(pathToEnvExample)) {
|
||||
const envExampleContents = await fs.readFile(pathToEnvExample, 'utf8')
|
||||
|
||||
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,
|
||||
exampleEnv = sanitizeEnv({
|
||||
contents: envExampleContents,
|
||||
databaseType,
|
||||
payloadSecret,
|
||||
databaseUri,
|
||||
)
|
||||
payloadSecret,
|
||||
})
|
||||
|
||||
await fs.writeFile(envExamplePath, updatedExampleContents)
|
||||
if (debugFlag) {
|
||||
debug(`.env.example file successfully updated`)
|
||||
debug(`.env.example file successfully read`)
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no .env file, create it using the .env.example content (if it exists)
|
||||
if (!fs.existsSync(envPath)) {
|
||||
const envContent = updateEnvExampleVariables(
|
||||
emptyEnvContent,
|
||||
const envContent = sanitizeEnv({
|
||||
contents: exampleEnv,
|
||||
databaseType,
|
||||
payloadSecret,
|
||||
databaseUri,
|
||||
)
|
||||
payloadSecret,
|
||||
})
|
||||
|
||||
await fs.writeFile(envPath, envContent)
|
||||
|
||||
if (debugFlag) {
|
||||
debug(`.env file successfully created`)
|
||||
}
|
||||
} else {
|
||||
// If the .env file already exists, sanitize it as-is
|
||||
const envContents = await fs.readFile(envPath, 'utf8')
|
||||
const mergedEnvs = envContents + '\n' + emptyEnvContent
|
||||
const updatedEnvContents = updateEnvExampleVariables(
|
||||
mergedEnvs,
|
||||
|
||||
const updatedEnvContents = sanitizeEnv({
|
||||
contents: envContents,
|
||||
databaseType,
|
||||
payloadSecret,
|
||||
databaseUri,
|
||||
)
|
||||
payloadSecret,
|
||||
})
|
||||
|
||||
await fs.writeFile(envPath, updatedEnvContents)
|
||||
|
||||
if (debugFlag) {
|
||||
debug(`.env file successfully updated`)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { ProjectTemplate } from '../types.js'
|
||||
|
||||
import { error, info } from '../utils/log.js'
|
||||
import { PACKAGE_VERSION } from './constants.js'
|
||||
|
||||
export function validateTemplate(templateName: string): boolean {
|
||||
export function validateTemplate({ templateName }: { templateName: string }): boolean {
|
||||
const validTemplates = getValidTemplates()
|
||||
if (!validTemplates.map((t) => t.name).includes(templateName)) {
|
||||
error(`'${templateName}' is not a valid template.`)
|
||||
@@ -20,13 +19,13 @@ export function getValidTemplates(): ProjectTemplate[] {
|
||||
name: 'blank',
|
||||
type: 'starter',
|
||||
description: 'Blank 3.0 Template',
|
||||
url: `https://github.com/payloadcms/payload/templates/blank#v${PACKAGE_VERSION}`,
|
||||
url: `https://github.com/payloadcms/payload/templates/blank#main`,
|
||||
},
|
||||
{
|
||||
name: 'website',
|
||||
type: 'starter',
|
||||
description: 'Website Template',
|
||||
url: `https://github.com/payloadcms/payload/templates/website#v${PACKAGE_VERSION}`,
|
||||
url: `https://github.com/payloadcms/payload/templates/website#main`,
|
||||
},
|
||||
{
|
||||
name: 'plugin',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import execa from 'execa'
|
||||
import fse from 'fs-extra'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
@@ -9,6 +8,7 @@ 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,15 +36,8 @@ export async function updatePayloadInProject(
|
||||
|
||||
const packageManager = await getPackageManager({ projectDir })
|
||||
|
||||
// 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')
|
||||
}
|
||||
// Fetch latest Payload version
|
||||
const latestPayloadVersion = await getLatestPackageVersion({ packageName: 'payload' })
|
||||
|
||||
if (payloadVersion === latestPayloadVersion) {
|
||||
return { message: `Payload v${payloadVersion} is already up to date.`, success: true }
|
||||
|
||||
@@ -8,7 +8,6 @@ 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'
|
||||
@@ -20,6 +19,7 @@ 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,13 +78,18 @@ 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 ')))
|
||||
@@ -200,7 +205,7 @@ export class Main {
|
||||
|
||||
const templateArg = this.args['--template']
|
||||
if (templateArg) {
|
||||
const valid = validateTemplate(templateArg)
|
||||
const valid = validateTemplate({ templateName: templateArg })
|
||||
if (!valid) {
|
||||
helpMessage()
|
||||
process.exit(1)
|
||||
@@ -230,7 +235,7 @@ export class Main {
|
||||
}
|
||||
|
||||
if (debugFlag) {
|
||||
debug(`Using ${exampleArg ? 'examples' : 'templates'} from git tag: v${PACKAGE_VERSION}`)
|
||||
debug(`Using ${exampleArg ? 'examples' : 'templates'} from git tag: v${LATEST_VERSION}`)
|
||||
}
|
||||
|
||||
if (!exampleArg) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.38.0",
|
||||
"version": "3.40.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -17,6 +17,7 @@
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
|
||||
@@ -20,7 +20,6 @@ type SearchParam = {
|
||||
|
||||
const subQueryOptions = {
|
||||
lean: true,
|
||||
limit: 50,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,7 +183,7 @@ export async function buildSearchParam({
|
||||
select[joinPath] = true
|
||||
}
|
||||
|
||||
const result = await SubModel.find(subQuery).lean().limit(50).select(select)
|
||||
const result = await SubModel.find(subQuery).lean().select(select)
|
||||
|
||||
const $in: unknown[] = []
|
||||
|
||||
|
||||
@@ -150,6 +150,18 @@ 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
|
||||
|
||||
@@ -417,7 +417,7 @@ export const sanitizeQueryValue = ({
|
||||
return buildExistsQuery(
|
||||
formattedValue,
|
||||
path,
|
||||
!['relationship', 'upload'].includes(field.type),
|
||||
!['checkbox', 'relationship', 'upload'].includes(field.type),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.38.0",
|
||||
"version": "3.40.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -17,6 +17,7 @@
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -85,10 +86,10 @@
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hyrious/esbuild-plugin-commonjs": "^0.2.4",
|
||||
"@hyrious/esbuild-plugin-commonjs": "0.2.6",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/to-snake-case": "1.0.0",
|
||||
"esbuild": "0.24.2",
|
||||
"esbuild": "0.25.5",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -51,13 +51,6 @@ export const connect: Connect = async function connect(
|
||||
) {
|
||||
const { hotReload } = options
|
||||
|
||||
this.schema = {
|
||||
pgSchema: this.pgSchema,
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
...this.enums,
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.pool) {
|
||||
this.pool = new this.pg.Pool(this.poolOptions)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.38.0",
|
||||
"version": "3.40.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -17,6 +17,7 @@
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
|
||||
@@ -15,11 +15,6 @@ export const connect: Connect = async function connect(
|
||||
) {
|
||||
const { hotReload } = options
|
||||
|
||||
this.schema = {
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.client) {
|
||||
this.client = createClient(this.clientConfig)
|
||||
|
||||
@@ -36,4 +36,9 @@ export const init: Init = async function init(this: SQLiteAdapter) {
|
||||
})
|
||||
|
||||
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })
|
||||
|
||||
this.schema = {
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.38.0",
|
||||
"version": "3.40.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -17,6 +17,7 @@
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -85,11 +86,11 @@
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hyrious/esbuild-plugin-commonjs": "^0.2.4",
|
||||
"@hyrious/esbuild-plugin-commonjs": "0.2.6",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/pg": "8.10.2",
|
||||
"@types/to-snake-case": "1.0.0",
|
||||
"esbuild": "0.24.2",
|
||||
"esbuild": "0.25.5",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -16,13 +16,6 @@ export const connect: Connect = async function connect(
|
||||
) {
|
||||
const { hotReload } = options
|
||||
|
||||
this.schema = {
|
||||
pgSchema: this.pgSchema,
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
...this.enums,
|
||||
}
|
||||
|
||||
try {
|
||||
const logger = this.logger || false
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.38.0",
|
||||
"version": "3.40.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
@@ -17,6 +17,7 @@
|
||||
"url": "https://payloadcms.com"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -63,6 +64,7 @@
|
||||
"@libsql/client": "0.14.0",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/pg": "8.10.2",
|
||||
"@types/prompts": "^2.4.5",
|
||||
"@types/to-snake-case": "1.0.0",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import type { Count, SanitizedCollectionConfig } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
import type { Count } from 'payload'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const count: Count = async function count(
|
||||
this: DrizzleAdapter,
|
||||
{ collection, locale, req, where: whereArg },
|
||||
{ collection: collectionSlug, locale, req, where: whereArg = {} },
|
||||
) {
|
||||
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
|
||||
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
|
||||
const { collectionConfig, tableName } = getCollection({ adapter: this, collectionSlug })
|
||||
|
||||
const db = await getTransaction(this, req)
|
||||
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import type { CountGlobalVersions, SanitizedGlobalConfig } from 'payload'
|
||||
import type { CountGlobalVersions } from 'payload'
|
||||
|
||||
import { buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getGlobal } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
|
||||
this: DrizzleAdapter,
|
||||
{ global, locale, req, where: whereArg },
|
||||
{ global: globalSlug, locale, req, where: whereArg = {} },
|
||||
) {
|
||||
const globalConfig: SanitizedGlobalConfig = this.payload.globals.config.find(
|
||||
({ slug }) => slug === global,
|
||||
)
|
||||
|
||||
const tableName = this.tableNameMap.get(
|
||||
`_${toSnakeCase(globalConfig.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
const { globalConfig, tableName } = getGlobal({ adapter: this, globalSlug, versions: true })
|
||||
|
||||
const db = await getTransaction(this, req)
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import type { CountVersions, SanitizedCollectionConfig } from 'payload'
|
||||
import type { CountVersions } from 'payload'
|
||||
|
||||
import { buildVersionCollectionFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const countVersions: CountVersions = async function countVersions(
|
||||
this: DrizzleAdapter,
|
||||
{ collection, locale, req, where: whereArg },
|
||||
{ collection: collectionSlug, locale, req, where: whereArg = {} },
|
||||
) {
|
||||
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
|
||||
|
||||
const tableName = this.tableNameMap.get(
|
||||
`_${toSnakeCase(collectionConfig.slug)}${this.versionsSuffix}`,
|
||||
)
|
||||
const { collectionConfig, tableName } = getCollection({
|
||||
adapter: this,
|
||||
collectionSlug,
|
||||
versions: true,
|
||||
})
|
||||
|
||||
const db = await getTransaction(this, req)
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { Create } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const create: Create = async function create(
|
||||
@@ -12,15 +11,13 @@ export const create: Create = async function create(
|
||||
{ collection: collectionSlug, data, req, returning, select },
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
const { collectionConfig, tableName } = getCollection({ adapter: this, collectionSlug })
|
||||
|
||||
const result = await upsertRow({
|
||||
adapter: this,
|
||||
data,
|
||||
db,
|
||||
fields: collection.flattenedFields,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
ignoreResult: returning === false,
|
||||
operation: 'create',
|
||||
req,
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import type { CreateGlobalArgs } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getGlobal } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export async function createGlobal<T extends Record<string, unknown>>(
|
||||
this: DrizzleAdapter,
|
||||
{ slug, data, req, returning }: CreateGlobalArgs,
|
||||
{ slug: globalSlug, data, req, returning }: CreateGlobalArgs,
|
||||
): Promise<T> {
|
||||
const db = await getTransaction(this, req)
|
||||
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
|
||||
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))
|
||||
const { globalConfig, tableName } = getGlobal({ adapter: this, globalSlug })
|
||||
|
||||
data.createdAt = new Date().toISOString()
|
||||
|
||||
@@ -30,10 +27,10 @@ export async function createGlobal<T extends Record<string, unknown>>(
|
||||
})
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
return null as unknown as T
|
||||
}
|
||||
|
||||
result.globalType = slug
|
||||
result.globalType = globalSlug
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { CreateGlobalVersionArgs, TypeWithID, TypeWithVersion } from 'paylo
|
||||
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { buildVersionGlobalFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getGlobal } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export async function createGlobalVersion<T extends TypeWithID>(
|
||||
@@ -25,9 +25,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
}: CreateGlobalVersionArgs,
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug)
|
||||
|
||||
const tableName = this.tableNameMap.get(`_${toSnakeCase(global.slug)}${this.versionsSuffix}`)
|
||||
const { globalConfig, tableName } = getGlobal({ adapter: this, globalSlug, versions: true })
|
||||
|
||||
const result = await upsertRow<TypeWithVersion<T>>({
|
||||
adapter: this,
|
||||
@@ -41,7 +39,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
version: versionData,
|
||||
},
|
||||
db,
|
||||
fields: buildVersionGlobalFields(this.payload.config, global, true),
|
||||
fields: buildVersionGlobalFields(this.payload.config, globalConfig, true),
|
||||
ignoreResult: returning === false ? 'idOnly' : false,
|
||||
operation: 'create',
|
||||
req,
|
||||
@@ -50,7 +48,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
})
|
||||
|
||||
const table = this.tables[tableName]
|
||||
if (global.versions.drafts) {
|
||||
if (globalConfig.versions.drafts) {
|
||||
await this.execute({
|
||||
db,
|
||||
sql: sql`
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { CreateVersionArgs, TypeWithID, TypeWithVersion } from 'payload'
|
||||
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { buildVersionCollectionFields } from 'payload'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export async function createVersion<T extends TypeWithID>(
|
||||
@@ -26,12 +26,13 @@ export async function createVersion<T extends TypeWithID>(
|
||||
}: CreateVersionArgs<T>,
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
const defaultTableName = toSnakeCase(collection.slug)
|
||||
const { collectionConfig, tableName } = getCollection({
|
||||
adapter: this,
|
||||
collectionSlug,
|
||||
versions: true,
|
||||
})
|
||||
|
||||
const tableName = this.tableNameMap.get(`_${defaultTableName}${this.versionsSuffix}`)
|
||||
|
||||
const version = { ...versionData }
|
||||
const version: Partial<TypeWithID> = { ...versionData }
|
||||
if (version.id) {
|
||||
delete version.id
|
||||
}
|
||||
@@ -51,7 +52,7 @@ export async function createVersion<T extends TypeWithID>(
|
||||
adapter: this,
|
||||
data,
|
||||
db,
|
||||
fields: buildVersionCollectionFields(this.payload.config, collection, true),
|
||||
fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
|
||||
operation: 'create',
|
||||
req,
|
||||
select,
|
||||
@@ -60,7 +61,7 @@ export async function createVersion<T extends TypeWithID>(
|
||||
|
||||
const table = this.tables[tableName]
|
||||
|
||||
if (collection.versions.drafts) {
|
||||
if (collectionConfig.versions.drafts) {
|
||||
await this.execute({
|
||||
db,
|
||||
sql: sql`
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import type { DeleteMany } from 'payload'
|
||||
|
||||
import { inArray } from 'drizzle-orm'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import { findMany } from './find/findMany.js'
|
||||
import { getCollection } from './utilities/getEntity.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const deleteMany: DeleteMany = async function deleteMany(
|
||||
this: DrizzleAdapter,
|
||||
{ collection, req, where },
|
||||
{ collection: collectionSlug, req, where },
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collectionConfig = this.payload.collections[collection].config
|
||||
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
|
||||
const { collectionConfig, tableName } = getCollection({ adapter: this, collectionSlug })
|
||||
|
||||
const result = await findMany({
|
||||
adapter: this,
|
||||
fields: collectionConfig.flattenedFields,
|
||||
joins: false,
|
||||
limit: 0,
|
||||
locale: req?.locale,
|
||||
locale: req?.locale ?? undefined,
|
||||
page: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
@@ -30,9 +28,9 @@ export const deleteMany: DeleteMany = async function deleteMany(
|
||||
where,
|
||||
})
|
||||
|
||||
const ids = []
|
||||
const ids: (number | string)[] = []
|
||||
|
||||
result.docs.forEach((data) => {
|
||||
result.docs.forEach((data: any) => {
|
||||
ids.push(data.id)
|
||||
})
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user