Compare commits

...

315 Commits

Author SHA1 Message Date
Alessio Gravili
6d541fd345 update 2025-03-10 14:23:41 -06:00
Alessio Gravili
77ab72a4cd turbo bug reproduction 2 2025-03-10 14:19:37 -06:00
Alessio Gravili
a08e960dda improve import 2025-02-24 19:18:31 -07:00
Alessio Gravili
40f8360bd9 reproduce turbo bug 2025-02-24 19:11:32 -07:00
Alessio Gravili
e8f2fcc395 refactor: remove obsolete scss folder copies 2025-02-24 17:18:19 -07:00
Alessio Gravili
e45b5ae54d fix tests 2025-02-24 17:10:36 -07:00
Alessio Gravili
537c608350 improve trackNetworkRequests helper 2025-02-24 17:09:01 -07:00
Alessio Gravili
769c9ac72a fix dependencies 2025-02-24 17:06:43 -07:00
Alessio Gravili
67b8117ff3 fix test dependency mismanagement 2025-02-24 16:23:01 -07:00
Alessio Gravili
c618809c1c fix command 2025-02-24 13:58:37 -07:00
Alessio Gravili
082928c63e ensure artifact name is not duplicative 2025-02-24 13:18:01 -07:00
Alessio Gravili
dce46520e9 remove unnecessary .filter 2025-02-24 12:58:37 -07:00
Alessio Gravili
934f657480 ci: run tests using turbopack 2025-02-24 12:56:58 -07:00
Patrik
09ca5143eb fix(plugin-nested-docs): fallback to empty string if useAsTitle field is undefined (#11338)
Updated `formatBreadcrumb` to fall back to an empty string if the
`useAsTitle` field for the document is undefined.

This handles cases where the field is optional or not filled out,
ensuring the label is never `undefined`.

Fixes #10377
2025-02-24 14:00:41 -05:00
Patrik
f1b005c4f5 fix(ui): object type field labels not displaying in search filter (#11366)
This PR ensures that when `titleField.label` is provided as an object,
it is correctly translated and displayed in the search filter's
placeholder.

Previously, the implementation only supported string values, which could
lead to issues with object type labels. With these changes, object type
labels will now properly show as intended in the search filter.

Fixes #11348
2025-02-24 13:38:21 -05:00
Alessio Gravili
dc9e8fa655 refactor: simplify running field hooks (#11372)
Previously, we were quite frequently using `.reduce()` to sequentially run field hooks. This PR replaces them with simple `for` loops, which is less overhead, less code, less confusing and simpler to understand.

Additionally, it refactors `mergeLocaleActions` which previously was unnecessarily complex. They no longer entail async code, thus we no longer have to juggle with promises
2025-02-24 18:37:33 +00:00
Jacob Fletcher
2477fc6c75 fix(ui): custom block labels stale when reordering blocks (#11367)
When blocks have custom row labels, those row labels become stale when
reordering blocks. After moving a block, for example, the row label will
jump back the original block until form state returns with the proper
rendering order. This is especially evident on slow networks.
2025-02-24 16:52:12 +00:00
Jarrod Flesch
37781808eb fix(plugin-multi-tenant): user access, thread field names through (#11365)
### What?
Two things:
1. Users unassigned to a tenant could not access their own account
2. Custom `tenantsArrayFieldName` and `tenantsArrayTenantFieldName`
configurations were not being used in all cases

### Why?
1. The access constraint provided by the plugin would not allow them to
make changes to their own account
2. `getUserTenantIDs` and `afterTenantDelete` were not using the custom
field names properly

### How?
1. Adds constraint for users allowing them to manage their own account
by default. Externally nothing has changed. If you need to lock your
users access control down you should do that just as you would without
this plugin.
2. Threads the field names through for usage.

Fixes https://github.com/payloadcms/payload/issues/11317
2025-02-24 11:17:09 -05:00
Jessica Chowdhury
d92c0009ed fix(plugin-nested-docs): corrects data shape of breadcrumbs returned in hooks (#10866)
### What?
The `plugin-nested-docs` returns an array of breadcrumbs - the
`resaveChildren` file accidentally processed the breadcrumbs twice, once
where the data is updated and once within the `populateBreadcrumbs`
function which was causing the objects to be double nested.

### How?
Removes the extra nesting from `resaveChildren` file and allows the
`populateBreadcrumbs` to return the final data.

Fixes #10855
2025-02-24 11:10:21 -05:00
Rafal Sypien
d32608649c docs: add info about changes in localized fields to v2 -> v3 migration guide (#11244)
### What?
Information that locale fields in database are changing to a simpler
data structure in v3.

### Why?
Simple data migration is not enough to get it working, I had to spend
quite some time to figure out migration files and still it required some
manual input. Maybe others will find it useful when starting a v3
migration.

### How?
I had to do some custom migration scripts on my own to get v3 to work
with existing pages data.

---------

Co-authored-by: Sasha <64744993+r1tsuu@users.noreply.github.com>
2025-02-24 17:36:11 +02:00
Paul
f9121c1a3a fix(ui): link element triggering clicks twice (#11362)
Fixes
https://github.com/payloadcms/payload/issues/11359#issuecomment-2678213414

The link element by using startTransitionRoute and manually calling
router.push would technically cause links to be clicked twice, by not
preventing default browser behaviour.

This caused a problem on clicking /create links as it hit the route
twice. Added a test making sure Create new doesn't lead to abnormally
increased document counts

Changes:
- Added `e.preventDefault()` in our Link element
- Added `preventDefault` as an optional prop to this element so that
people can handle it on their own if needed via a custom `onClick`
2025-02-24 14:49:52 +00:00
Jarrod Flesch
f477e0e3c4 feat(plugin-multi-tenant): export useTenantSelection hook for public usage (#11364)
Exports the `useTenantSelection` hook from the multi-tenant plugin, this
way other users can import and use the hook along with it's methods.

Can be imported:
```ts
import { useTenantSelection } from '@payloadcms/plugin-multi-tenant/client'
```

The context returned:

```ts
type ContextType = {
  /**
   * Array of options to select from
   */
  options: OptionObject[]
  /**
   * The currently selected tenant ID
   */
  selectedTenantID: number | string | undefined
  /**
   * Prevents a refresh when the tenant is changed
   *
   * If not switching tenants while viewing a "global", set to true
   */
  setPreventRefreshOnChange: React.Dispatch<React.SetStateAction<boolean>>
  /**
   * Sets the selected tenant ID
   * 
   * @param args.id - The ID of the tenant to select
   * @param args.refresh - Whether to refresh the page after changing the tenant
   */
  setTenant: (args: { id: number | string | undefined; refresh?: boolean }) => void
}
```
2025-02-24 09:29:45 -05:00
Sasha
4224c68002 docs: fix links to react hooks (#11344) 2025-02-22 13:23:00 +00:00
Paul
b014416584 feat: add support for not_like operation (#11326)
This PR adds support for `not_like` as a query operation. Functions just
as a negative to `like`, uses `ilike` in postgres and `like` in sqlite
2025-02-22 00:32:37 +00:00
Alessio Gravili
1725af5e3a fix(next): use correct hmr url if assetPrefix is set in next config (#10859)
The next.js assetPrefix needs to be included in the websocket URL.

Previously, we were appending both assetPrefix and basePath, which is incorrect. `assetPrefix` overrides `basePath` if both are set. This PR mimics the way Next.js connects to the HMR server.

Sources:
- https://github.com/AlessioGr/next.js/blob/canary/packages/next/src/server/lib/router-server.ts#L688
- https://github.com/AlessioGr/next.js/blob/canary/packages/next/src/server/config.ts#L322
- https://github.com/AlessioGr/next.js/blob/canary/packages/next/src/client/components/react-dev-overlay/app/client-entry.tsx
2025-02-22 00:27:07 +00:00
Sasha
a13d4fe5c6 perf: remove deep copy of the entire sanitized entity config in configToJSONSchema (#11342)
Small performance improvement for types generation and anywhere else
`configToJSONSchema` can be used.

We did a deep copy of the whole sanitized entity config, which can be
expensive. Now, to do the needed mutation of `flattenedFields`, we just
create a new reference of `flattenedFields` for that.
2025-02-22 01:59:50 +02:00
Alessio Gravili
3ffc268438 fix: safely access config.blocks during type generation (#11320)
A discord user reported a "`TypeError: config.blocks is not iterable`" error when generating types: https://discord.com/channels/967097582721572934/1342383112289517578/1342383112289517578

To prevent that error, this PR ensures config.blocks is safely accessed during type generation
2025-02-21 23:26:28 +00:00
Jacob Fletcher
d766b1904c feat(ui): threads row data through list drawer onSelect callback (#11339)
When rendering a list drawer, you can pass a custom `onSelect` callback
to execute when the user clicks on the linked cell within the table. The
underlying handler, however, only passes the `docID` and
`collectionSlug` args through the callback, rather than the document
itself. This makes it impossible to perform side-effects that require
the data of the row that was selected.

Instances of this callback were also largely untyped.

Needed for #11330.
2025-02-21 17:08:05 -05:00
Jacob Fletcher
f31568c69c refactor(ui): moves bulk edit controls (#11332)
Bulk edit controls are currently displayed within the search bar of the
list view. This doesn't make sense from a UX perspective, as the current
selection is displayed somewhere else entirely. These controls also take
up a lot of visual real estate which is beginning to get overused
especially after the introduction of "list menu items" in #11230, and
the potential introduction of "saved filters" controls in #11330.

Now, they are rendered contextually _alongside_ the selection count. To
make room for these new controls, they are displayed in plain text and
the entity labels have been removed from the selection count.
2025-02-21 15:06:30 -05:00
Jacob Fletcher
a8bec9a1b2 fix(ui): only show bulk select all when count is less than total (#11329)
It doesn't make sense to display "select all" when bulk editing if your
current selection already contains all records.
2025-02-21 13:42:47 -05:00
Patrik
5d7be15c52 templates: update docker-compose to use postgres by default in postgres specific templates (#11328)
### What?

Updated `docker-compose.yml` to use `Postgres` by default, with
`MongoDB` commented out in the templates that are by default using the
postgres adapter.

Fixes #11322
2025-02-21 13:15:54 -05:00
Alessio Gravili
0058f82d87 perf: add limit: 1 and pagination: false to various payload queries (#11319)
`payload.find` queries can be made faster by specifying `limit: 1` and `pagination: false` when only the first document is needed. This PR applies those options to various queries to improve performance.
2025-02-21 11:09:40 -07:00
Tib
6ff380ce59 docs: update outdated docs/rich-text/overview feature names (#11324)
Features was outdated
2025-02-21 10:29:57 -07:00
Sasha
f779e48a58 fix: working bin configuration for custom scripts (#11294)
Previously, the `bin` configuration wasn't working at all.
Possibly because in an ESM environment this cannot work, because
`import` always returns an object with a default export under the
`module` key.
```ts
const script: BinScript = await import(pathToFileURL(userBinScript.scriptPath).toString())
await script(config)
```
Now, this works, but you must define a `script` export from your file.
Attached an integration test that asserts that it actually works. Added
documentation on how to use it, as previously it was missing.
This can be also helpful for plugins.

### Documentation

Using the `bin` configuration property, you can inject your own scripts
to `npx payload`.
Example for `pnpm payload seed`:

Step 1: create `seed.ts` file in the same folder with
`payload.config.ts` with:

```ts
import type { SanitizedConfig } from 'payload'

import payload from 'payload'

// Script must define a "script" function export that accepts the sanitized config
export const script = async (config: SanitizedConfig) => {
  await payload.init({ config })
  await payload.create({ collection: 'pages', data: { title: 'my title' } })
  payload.logger.info('Succesffully seeded!')
  process.exit(0)
}
```

Step 2: add the `seed` script to `bin`:
```ts
export default buildConfig({
  bin: [
    {
      scriptPath: path.resolve(dirname, 'seed.ts'),
      key: 'seed',
    },
  ],
})
```

Now you can run the script using:
```sh
pnpm payload seed
```
2025-02-21 18:15:27 +02:00
Sasha
1dc748d341 perf(db-mongodb): remove JSON.parse(JSON.stringify) copying of results (#11293)
Improves performance and optimizes memory usage for mongodb adapter by
cutting down copying of results via `JSON.parse(JSON.stringify())`.
Instead, `transform` does necessary transformations (`ObjectID` ->
`string,` `Date` -> `string`) without any copying
2025-02-21 17:31:24 +02:00
Alessio Gravili
c7c5018675 perf(next): reduce getNavPrefs calls from 3 to 1 per page load (#11318)
Previously, we were calling `getNavPrefs` (a payload.find call) three times for every single page load.

This PR:

1. Ensures that `getNavPrefs` is called only once per page load, reducing two unnecessary `payload.find` calls every time a page is loaded or navigated to.
2. Adds `pagination: false` to the `payload.find` call, making it more efficient and improving performance.

## How?

We were using React's cache to ensure that navigation preferences (`getNavPrefs`) were fetched only once per request. However, this wasn't working as expected because the first argument of `getNavPrefs` was an object. Each time it was called, a new object reference was passed, preventing React from caching it properly.

To fix this, this PR ensures that only primitive values are used as arguments for caching, following best practices and making the cache function work as intended.
2025-02-21 05:17:39 +00:00
Alessio Gravili
9728d80592 fix: db transaction errors caused by checkDocumentLockStatus (#11287)
Just like https://github.com/payloadcms/payload/pull/11269, we stop passing through `req` to db operations in `checkDocumentLockStatus`.

After extensive testing, this seems to get rid of all transaction errors that occurred when I was testing autosave against a remote mongo DB.

We keep the `req` for postgres, as it mysteriously breaks in CI - this cannot be reproduced locally
2025-02-20 20:01:15 -07:00
Sasha
0594701004 chore: add JSDoc for globals Local API operations (#11313)
Continuation of https://github.com/payloadcms/payload/pull/11265 for
globals.
Documents all the possible properties of globals Local API operations
with JSDoc.
2025-02-21 04:33:36 +02:00
Alessio Gravili
845c647ebc perf: ensure fetching and updating preferences doesn't cause transaction errors and is done correctly (#11311)
## getPreferences function caching

Our `getPreferences` function used in the ui package is now wrapped in react cache, to minimize the amount of times it runs on a single request. This mimics the behavior of our other `getPreferences` function in the next package.

## getPreferences  incorrect behavior

The `getPreferences` function in the next package was passing through the incorrect user slug. This would not have been noticeable in projects with just one users collection, but might break in projects with multiple users collections.

## getPreferences performance optimization

This PR adds `pagination: false` to the getPreferences payload.find() call, which will speed up the query.

## upsertPreferences transaction errors

Due to the potential of preference upsert operations running in parallel (e.g. when switching locales), this PR disables transactions in the preferences creation / update calls. This fixes the transaction errors reported in https://github.com/payloadcms/payload/issues/11310
2025-02-21 01:53:56 +00:00
Sasha
f6f6a1dc99 chore: cut down logging noise when file was not found on the disk (#11295)
When `/uploads/file/file-path.jpg` endpoint is requested, and
`file-path.jpg` _was found_ in the database, but was not found on the
disk, the error like this is printed to the console:

```
[03:34:54] ERROR: ENOENT: no such file or directory, stat '/Users/sasha/work/payload/test/uploads/collections/Upload1/uploads/Screenshot 2025-02-19 at 18.50.46.png'
    err: {
      "type": "Error",
      "message": "ENOENT: no such file or directory, stat '/Users/sasha/work/payload/test/uploads/collections/Upload1/uploads/Screenshot 2025-02-19 at 18.50.46.png'",
      "stack":
          Error: ENOENT: no such file or directory, stat '/Users/sasha/work/payload/test/uploads/collections/Upload1/uploads/Screenshot 2025-02-19 at 18.50.46.png'
              at async Object.stat (node:internal/fs/promises:1037:18)
              at async getFileHandler (webpack-internal:///(rsc)/./packages/payload/src/uploads/endpoints/getFile.ts:59:19)
              at async handleEndpoints (webpack-internal:///(rsc)/./packages/payload/src/utilities/handleEndpoints.ts:178:26)
              at async eval (webpack-internal:///(rsc)/./packages/next/src/routes/rest/index.ts:27:26)
              at async AppRouteRouteModule.do (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js:10:32847)
              at async AppRouteRouteModule.handle (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js:10:39868)
              at async doRender (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/base-server.js:1452:42)
              at async responseGenerator (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/base-server.js:1822:28)
              at async DevServer.renderToResponseWithComponentsImpl (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/base-server.js:1832:28)
              at async DevServer.renderPageComponent (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/base-server.js:2259:24)
              at async DevServer.renderToResponseImpl (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/base-server.js:2297:32)
              at async DevServer.pipeImpl (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/base-server.js:959:25)
              at async NextNodeServer.handleCatchallRenderRequest (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/next-server.js:281:17)
              at async DevServer.handleRequestImpl (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/base-server.js:853:17)
              at async /Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/dev/next-dev-server.js:371:20
              at async Span.traceAsyncFn (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/trace/trace.js:153:20)
              at async DevServer.handleRequest (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/dev/next-dev-server.js:368:24)
              at async invokeRender (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/lib/router-server.js:230:21)
              at async handleRequest (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/lib/router-server.js:408:24)
              at async NextCustomServer.requestHandlerImpl (/Users/sasha/work/payload/node_modules/.pnpm/next@15.1.5_@opentelemetry+api@1.9.0_@playwright+test@1.50.0_babel-plugin-macros@3.1.0_babel-_vtyh6lbrnb3jbtpvhe5wg2qgzq/node_modules/next/dist/server/lib/router-server.js:432:13)
              at async Server.<anonymous> (file:///Users/sasha/work/payload/test/dev.ts:1:2848)
      "errno": -2,
      "code": "ENOENT",
      "syscall": "stat",
      "path": "/Users/sasha/work/payload/test/uploads/collections/Upload1/uploads/Screenshot 2025-02-19 at 18.50.46.png"
    }
```
Which is quite noisy as we understand why this error happens (and it
might be intentional when you load production DB to local and don't have
any files)

Now, the logging here in case of `ENOENT`  is simplified to this:
```
[03:43:35] ERROR: File Screenshot 2025-02-19 at 18.50.46.png for collection uploads-1 is missing on the disk. Expected path: /Users/sasha/work/payload/test/uploads/collections/Upload1/uploads/Screenshot 2025-02-19 at 18.50.46.png
```


Fixes https://github.com/payloadcms/payload/issues/6246
2025-02-21 02:05:10 +02:00
Sasha
ee5e96a965 chore: add JSDoc for collection Local API operations properties (#11265)
### What?
Adds JSDoc for options of all collection Local API operations
_Every_ property now is documented there, even those that aren't on our
website docs.

### Why?
This is useful to have, now you can hover over any property to see what
it does in your editor.
Some properties also link to the documentation website directly for more
info.

### How?
Updates every collection operation arguments definition with JSDoc.

For globals will be a separate PR.
2025-02-21 02:04:52 +02:00
Said Akhrarov
22f61ad79e feat(ui): adds support for block groups (#11239)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR introduces support for the `admin.group` property in block
configs. This property enables blocks to be grouped under a common,
potentially localized, label in the block drawer component. This makes
it easier to sort through large collections of blocks. Previously, all
blocks would be in one common layout.

This PR also encompasses documentation changes and e2e tests to check
for the rendering of group labels.

### Why?
To make it easier to organize many blocks in block fields.

### How?
By introducing a new `admin.group` property in block configs and
assembling them in the blocks drawer component.

Before:

![Editing-Block-Field-Payload--before](https://github.com/user-attachments/assets/fb0c887b-ee47-46a1-a249-c4a4b7a5c13c)

After:

![Editing-Block-Field-Payload--after](https://github.com/user-attachments/assets/046d5a6f-3108-4464-ac69-8b7afcf27094)

Demo:

[Editing---Block-Field---Payload-groups-demo.webm](https://github.com/user-attachments/assets/2b351dc1-0d14-4a5b-ae71-bcd31fbb23df)

Addresses #5609
2025-02-20 19:51:47 +00:00
Jarrod Flesch
460d50baa3 fix(ui): confirmation modal should build off of drawerDepth, instead … (#11305)
When the new ConfirmationModal is used outside of the context of an
EditDepthProvider, it stacks behind currently open modals. This is
apparent when using it in custom views.

The fix is to build the confirm modal's depth off of drawerDepth instead
of editDepth.
2025-02-20 14:40:29 -05:00
Alessio Gravili
76bd05cc5d perf(next): avoid unnecessary upsertPreferences call on page load (#11302)
`getRequestLocale` => `upsertPreferences` is already called as part of `initReq`, yet we were still unnecessarily calling `getRequestLocale` afterwards, which potentially resulted in at least one unnecessary `payload.find()` or `payload.update()` call.
2025-02-20 19:00:31 +00:00
Boyan Bratvanov
af92c1562c docs: fix formatting in field hooks table (#11300)
The line for `siblingFields` has an extra newline and space that's
breaking the table formatting.

https://payloadcms.com/docs/hooks/fields
2025-02-20 09:19:46 -05:00
Jessica Chowdhury
6fad5d7c0a chore(translations): adds missing client keys and removes translated validation errors (#10841)
### What?
Fixes translation errors that are thrown when JSON field validation
outputs an error.

### How?
Removes translation function `t()` from wrapping the errors and adds
translation keys that were missing from `clientKeys.ts`.

Fixes #10543
2025-02-20 12:49:15 +00:00
Jessica Chowdhury
c05f10abbc chore: passes allowCreate into list drawer and adds test (#11284)
### What?
We had an `allowCreate` prop for the list drawer that doesn't do
anything. This PR passes the prop through so it can be used.

### How?
Passes `allowCreate` down to the list view and ties it with
`hasCreatePermission`

#### Testing
- Use `admin` test suite and `withListDrawer` collection.
- Test added to the `admin/e2e/list-view`.

Fixes #11246
2025-02-20 12:31:36 +00:00
Jessica Chowdhury
26163a7535 fix(next): uses assetPrefix from next config in webpack-hmr URL (#11229)
### What?
Adding `assetPrefix` to the `next.config` prevents the hot module
reloading functionality.

### Why & How?
Need to incorporate `assetPrefix` into the URL generated for webpack
HMR.

Fixes #11150

#### Testing
1. Add `assetPrefix: '/test'` to the `next.config.mjs` in the root
folder
2. Run `pnpm test _community`
3. Go to the `_community/collections/posts` config and change a field
4. Open post collection in browser and see no change (if this PR is
checked out then you _**will**_ see the change)
2025-02-20 12:30:44 +00:00
Patrik
c517e7e688 docs: removes outdated rateLimit option (#11291)
### What?

This PR removes references to the `rateLimit` option from the
documentation, as it was deprecated in Payload v3.

Since Payload now runs on Next.js, which are often deployed
serverlessly, built-in rate limiting is no longer supported.

Users are encouraged to implement rate limiting at the load balancer,
proxy level, or use services like Cloudflare.

Fixes #10321
2025-02-19 16:12:04 -05:00
Alessio Gravili
563c21bec0 feat(richtext-lexical): support single-quoted jsx property values in mdx converter (#11290)
The following MDX:

```tsx
<Banner type='info'>
  Hello
</Banner>
```

was not able to be parsed by the lexical mdx converter, as the jsx props string extractor did not support the single quotes around the `info` string.
2025-02-19 21:11:50 +00:00
Alessio Gravili
b1e9aa53ab docs: fix invalid jsx in banner block (#11289)
Single quote strings are not supported by our jsx parser
2025-02-19 13:41:22 -07:00
Alessio Gravili
26127567b6 fix(richtext-lexical): incorrect UploadData types (#11288)
This PR fixes the `UploadData` type that was weakened in a previous PR, causing a breaking change. It also improves the newly added `UploadDataImproved` type by bringing back its support for generated types and using the `UploadCollectionSlug` type helper to restrict collection slugs to upload-enabled collections.
2025-02-19 20:22:44 +00:00
Elliot DeNolf
f3161f9405 chore(release): v3.24.0 [skip ci] 2025-02-19 13:37:26 -05:00
Paul
e83318b156 fix(ui): minor issues with tabs and publish buttons when in RTL (#11282)
Fixes https://github.com/payloadcms/payload/issues/11162

Our tabs had wrong spacing in RTL.

The publish button with its dropdown had its borders and border radiuses
on the wrong side for RTL and fixed a minor issue in website template
around RTL margins.

Now publish button looks as expected in RTL:

![image](https://github.com/user-attachments/assets/023d27c9-dc14-4aa1-a5e7-b48f498921fd)
2025-02-19 17:18:01 +00:00
Sasha
009e9085fc fix(ui): turbopack with the latest next.js canary [skip lint] (#11280)
Fixes https://github.com/payloadcms/payload/issues/11211

Disables prepending `"use client"` to `.map` files
2025-02-19 18:55:04 +02:00
Dan Ribbens
9fc1cd0d24 fix(ui): disabledLocalStrategy.enableFields missing email/username fields (#11232)
When using `disabledLocalStrategy.enableFields`, it was impossible to
create a user from the admin panel because the username or email field
was missing.

![Screenshot 2025-02-17
133851](https://github.com/user-attachments/assets/f84ac74e-a3ce-4428-81b5-7135fc1cb917)

---------

Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
2025-02-19 11:43:40 -05:00
Jarrod Flesch
0651ae0727 fix: versions not loading properly (#11256)
### What?
The admin panel was not respecting where constraints returned from the
readAccess function.

### Why?
`getEntityPolicies` was always using `find` when looping over the
operations, but `readVersions` should be using `findVersions`.

### How?
When the operation is `readVersions` run the `findVersions` operation.

Fixes https://github.com/payloadcms/payload/issues/11240
2025-02-19 10:22:31 -05:00
Dan Ribbens
618624e110 fix(ui): unsaved changes allows for scheduled publish missing changes (#11001)
### What?
When you first edit a document and then open the Schedule publish
drawer, you can schedule publish changes but the current changes made to
the form won't be included.

### Why?
The UX does not make it clear that the changes you have in the form are
not actually going to be published.

### How?
Instead of allowing that we just disable the Schedule Publish drawer
toggler so that users are forced to save a draft first.

In addition to the above, this change also passes a defaultType so that
an already published document will default the radio type have
"Unpublish" selected.
2025-02-19 10:10:29 -05:00
felismargarita
cd48904798 fix(next): imports toast from @payloadcms/ui (#11279)
Restoring a version has two types of messages, success and error, but no
matter if this action is a success or a failure, the toast message is
never displayed.

The fix is to import the toast from `@payloadcms/ui` instead of `sonner`
directly.

Fixes #11059
2025-02-19 09:49:00 -05:00
Paul
8b0ae902e7 fix(ui): timezone issue related to date only fields in Pacific timezones (#11203)
Fixes https://github.com/payloadcms/payload/issues/10962

This fix addresses fields with timezones enabled specifically for not
time pickers. If all you want to do is pick a date such as 14th Feb, it
would store the incorrect version and display a date in the future for
people in the Pacific.

This is because Auckland is +12 offset, but +13 with Daylight Savings
Time. In our date picker we try to normalise date pickers with no time
to 12pm and so half the year we ended up pushing dates visually to the
next day for people in the pacific only. Other regions were not affected
by this because their offset would be less than 12.

This PR fixes this by ensuring that our dates are always normalised to
selected timezone's 12pm date to UTC.

There's also additional tests for these two fields from 3 main locations
to cover a wider range of possible timezones.
2025-02-19 14:46:05 +00:00
Fredrik
80b33adf6b feat(ui): enable specific css selectors on the localizer per locale
Adds a `locale-${localeCode}` data attribute to the localizer label and buttons.
2025-02-19 14:41:01 +00:00
Alessio Gravili
9b8f8d70ca fix: db transaction errors caused by checkDocumentLockStatus (#11273)
Just like https://github.com/payloadcms/payload/pull/11269, we stop
passing through `req` to db operations in `checkDocumentLockStatus`.

After extensive testing, this seems to get rid of all transaction errors
that occurred when I was testing autosave against a remote mongo DB.
2025-02-19 14:18:53 +00:00
Jacob Fletcher
af5554981c refactor(ui): simplifies confirmation modal callback (#11278)
Removes unnecessary callback args from the `onConfirm` callback in the
new `ConfirmationModal` component. Now, the component will close and
reset `isConfirming` state for itself.
2025-02-19 09:18:37 -05:00
Germán Jabloñski
7024da8be3 docs: fix variable name typo in usePayloadAPI (error → isError) (#11249) 2025-02-19 08:52:53 -05:00
Paul
acead1083b feat: add support for interfaceName on radio and select fields to create reusable top level types (#11277)
Adds support for `interfaceName` on radio and select fields. Adding this
property will extract your provided options into a top level type for
re-use.


![image](https://github.com/user-attachments/assets/be5c3e17-5127-4546-a778-d3aa801dec90)

Added types test to make sure assignment is consistent.
2025-02-19 13:51:03 +00:00
Tib
bf103cc025 docs: fix typo in cors (#11266)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
2025-02-19 12:51:24 +00:00
Paul
8bbe7bcbbe feat(translations): add support for lithuanian (#11243)
Adds support for lithuanian language
2025-02-19 11:04:16 +00:00
Jacob Fletcher
bd8ced1b60 feat(ui): confirmation modal (#11271)
There are nearly a dozen independent implementations of the same modal
spread throughout the admin panel and various plugins. These modals are
used to confirm or cancel an action, such as deleting a document, bulk
publishing, etc. Each of these instances is nearly identical, leading to
unnecessary development efforts when creating them, inconsistent UI, and
duplicative stylesheets.

Everything is now standardized behind a new `ConfirmationModal`
component. This modal comes with a standard API that is flexible enough
to replace nearly every instance. This component has also been exported
for reuse.

Here is a basic example of how to use it:

```tsx
'use client'
import { ConfirmationModal, useModal } from '@payloadcms/ui'
import React, { Fragment } from 'react'

const modalSlug = 'my-confirmation-modal'

export function MyComponent() {
  const { openModal } = useModal()

  return (
    <Fragment>
      <button
        onClick={() => {
          openModal(modalSlug)
        }}
        type="button"
      >
        Do something
      </button>
      <ConfirmationModal
        heading="Are you sure?"
        body="Confirm or cancel before proceeding."
        modalSlug={modalSlug}
        onConfirm={({ closeConfirmationModal, setConfirming }) => {
          // do something
          setConfirming(false)
          closeConfirmationModal()
        }}
      />
    </Fragment>
  )
}
```
2025-02-19 02:27:03 -05:00
Alessio Gravili
132852290a fix(ui): database errors when running autosave and ensure autosave doesn't run unnecessarily (#11270)
## Change 1 - database errors when running autosave

The previous autosave implementation allowed multiple autosave fetch
calls (=> save document draft) to run in parallel. While the
AbortController aborted previous autosave calls if a new one comes in in
order to only process the latest one, this had one flaw:

Using the AbortController to abort the autosave call only aborted the
`fetch` call - it did not however abort the database operation that may
have started as part of this fetch call. If you then started a new
autosave call, this will start yet another database operation on the
backend, resulting in two database operations that would be running at
the same time.

This has caused a lot of transaction errors that were only noticeable
when connected to a slower, remote database. This PR removes the
AbortController and ensures that the previous autosave operation is
properly awaited before starting a new one, while still discarding
outdated autosave requests from the queue **that have not started yet**.

Additionally, it cleans up the AutoSave component to make it more
readable.

## Change 2 - ensure autosave doesn't run unnecessarily

If connected to a slower backend or database, one change in a document
may trigger two autosave operations instead of just one. This is how it
could happen:

1. Type something => formstate changes => autosave is triggered
2. 200ms later: form state request is triggered. Autosave is still
processing
3. 100ms later: form state comes back from server => local form state is
updated => another autosave is triggered
4. First autosave is aborted - this lead to a browser error. This PR
ensures that that error is no longer surfaced to the user
5. Another autosave is started

This PR adds additional checks to ONLY trigger an autosave if the form
DATA (not the entire form state itself) changes. Previously, it ran
every time the object reference of the form state changes. This includes
changes that do not affect the form data, like `field.valid`. =>
Basically every time form state comes back from the server, we were
triggering another, unnecessary autosave
2025-02-19 03:38:32 +00:00
Alessio Gravili
7f5aaad6a5 fix(ui): do not pass req in handleFormStateLocking (#11269)
Not passing through `req` ensures that the db operations in
`handleFormStateLocking` run independently, preventing them from being
part of the same transaction. Since locked document operations don't
really require transactional consistency, this change helps avoid
unnecessary transaction errors that have previously occurred here.
2025-02-19 02:43:20 +00:00
Sasha
38c1c113ca docs: remove outdated res parameter in login and resetPassword operations (#11268)
Fixes https://github.com/payloadcms/payload/issues/9829

As described in the issue, the `res` parameter does no more exist for
these operations. Additionally, marks `req` as an optional property.
2025-02-19 03:59:05 +02:00
Alessio Gravili
7922d66181 fix(db-mongodb): properly handle document notfound cases for update and delete operations (#11267)
In the `findOne` db operation, we return `null` if the document was not
found.

For single-document delete and update operations, if the document you
wanted to update is not found, the following runtime error is thrown
instead: `Cannot read properties of null (reading '_id')`.

This PR correctly handles these cases and returns `null` from the db
method, just like the `findOne` operation.
2025-02-19 01:19:59 +00:00
Sasha
0d7cf3fca2 docs: update join field docs (#11264)
### What?
Updates the join field documentation. 
Mentions:
* Now you can specify an array of `collection` -
https://github.com/payloadcms/payload/pull/10919
* Querying limitation for join fields, planned
https://github.com/payloadcms/payload/discussions/9683
* Querying limitation for joined documents when the join field has an
array of `collection` for fields inside arrays and blocks.

### Why?
To have up to date documentation for an array of `collection` and so
users can know about limitations.

### How?
Updates the file on path `docs/fields/join.mdx`.
2025-02-19 00:31:15 +02:00
Jacob Fletcher
8166784ba2 test: blocks field helpers (#11259)
Similar to the goals of #11026. Adds helper utilities to make
interacting with the blocks field easier within e2e tests. This will
also standardize common functionality across tests and reduce the
overall lines of code for each, making them easier to navigate and
digest.

The following helpers are now available:

- `openBlocksDrawer`: self-explanatory
- `addBlock`: opens the blocks drawer and selects the given block
- `reorderBlocks`: similar to `reorderColumn`, moves blocks using the
drag handle
- `removeAllBlocks`: iterates all rows of a given blocks field and
removes them
2025-02-18 15:48:57 -05:00
Sasha
6d36a28cdc feat: join field across many collections (#10919)
This feature allows you to specify `collection` for the join field as
array.
This can be useful for example to describe relationship linking like
this:
```ts
{
  slug: 'folders',
  fields: [
    {
      type: 'join',
      on: 'folder',
      collection: ['files', 'documents', 'folders'],
      name: 'children',
    },
    {
      type: 'relationship',
      relationTo: 'folders',
      name: 'folder',
    },
  ],
},
{
  slug: 'files',
  upload: true,
  fields: [
    {
      type: 'relationship',
      relationTo: 'folders',
      name: 'folder',
    },
  ],
},
{
  slug: 'documents',
  fields: [
    {
      type: 'relationship',
      relationTo: 'folders',
      name: 'folder',
    },
  ],
},
```

Documents and files can be placed to folders and folders themselves can
be nested to other folders (root folders just have `folder` as `null`).

Output type of `Folder`:
```ts
export interface Folder {
  id: string;
  children?: {
    docs?:
      | (
          | {
              relationTo?: 'files';
              value: string | File;
            }
          | {
              relationTo?: 'documents';
              value: string | Document;
            }
          | {
              relationTo?: 'folders';
              value: string | Folder;
            }
        )[]
      | null;
    hasNextPage?: boolean | null;
  } | null;
  folder?: (string | null) | Folder;
  updatedAt: string;
  createdAt: string;
}
```

While you could instead have many join fields (for example
`childrenFolders`, `childrenFiles`) etc - this doesn't allow you to
sort/filter and paginate things across many collections, which isn't
trivial. With SQL we use `UNION ALL` query to achieve that.

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-02-18 21:53:45 +02:00
Sasha
88548fcbe6 fix(db-postgres): querying other collections via relationships inside blocks (#11255)
### What?
Previously, in postgres query like:
```ts
const result = await payload.find({
  collection: 'blocks',
  where: { 'blocks.director.name': { equals: 'Test Director' } },
})
```
where `blocks` is a blocks field, `director` is a relationship field and
`name` is a text field inside `directors`, failed with:

![image](https://github.com/user-attachments/assets/f4b62b69-bd17-4ef0-9f0c-08057e9f2d57)

### Why?
The generated query before was a bit wrong.
Before:
```sql
select distinct
  "blocks"."id",
  "blocks"."created_at",
  "blocks"."created_at"
from
  "blocks"
  left join "directors" "a5ad426a_eda4_4067_af7e_5b294d7f0968" on "a5ad426a_eda4_4067_af7e_5b294d7f0968"."id" = "blocks_blocks_some"."director_id"
   left join "blocks_blocks_some" on "blocks"."id" = "blocks_blocks_some"."_parent_id"
where
  "a5ad426a_eda4_4067_af7e_5b294d7f0968"."name" = 'Test Director'
order by
  "blocks"."created_at" desc
limit
	10
```
Notice `left join directors` _before_ join of `blocks_blocks_some`.
`blocks_blocks_some` doesn't exist yet, this PR changes so now we
generate
```sql
select distinct
  "blocks"."id",
  "blocks"."created_at",
  "blocks"."created_at"
from
  "blocks"
  left join "blocks_blocks_some" on "blocks"."id" = "blocks_blocks_some"."_parent_id"
  left join "directors" "a5ad426a_eda4_4067_af7e_5b294d7f0968" on "a5ad426a_eda4_4067_af7e_5b294d7f0968"."id" = "blocks_blocks_some"."director_id"
where
  "a5ad426a_eda4_4067_af7e_5b294d7f0968"."name" = 'Test Director'
order by
  "blocks"."created_at" desc
limit
	10
```
2025-02-18 14:51:17 -05:00
Paul
06debf5e14 fix(ui): issues with prevent leave and autosave when the form is submitted but invalid (#11233)
Fixes https://github.com/payloadcms/payload/issues/11224
Fixes https://github.com/payloadcms/payload/issues/10492

This PR fixes a few weird behaviours when `validate: true` is set on drafts:
- when autosave is on and you submit an invalid form it would get stuck in an infinite loop
- PreventLeave would not trigger for submitted but invalid forms leading to potential data loss

Changes:
- Adds e2e tests for the above scenarios
- Adds a new `isValid` flag on the `Form` context provider to signal globally if the form is in a valid or invalid state
  - Components like Autosave will manage this internally since it manages its own submission flow as well
- Adds PreventLeave to Autosave too for when form is invalid meaning data hasn't been actually saved so we want to prevent the user accidentally losing data by reloading or closing the page


The following tests have been added
![image](https://github.com/user-attachments/assets/db208aa4-6ed6-4287-b200-59575cd3c9d0)
2025-02-18 12:12:41 -07:00
Alessio Gravili
ede7bd7b4b docs: add missing jsdocs to version config (#11258) 2025-02-18 18:21:15 +00:00
Paul
1c4eba41b7 fix(ui): allow selectinputs to reset to their initial values if theres no provided value (#11252)
When reusing the SelectInput component from the UI package, if you set
value to `''` it will continue to display the previously selected value
instead of clearing out the field as expected.

The ReactSelect component doesn't behave in this way and instead will
clear out the field.

This fix addresses this difference by resetting `valueToRender` inside
the SelectInput to null.
2025-02-18 16:40:29 +00:00
Alessio Gravili
313ff047df perf: optimize permissions calculation with lots of blocks (#11236)
This PR optimizes permissions calculation for block references, by
calculating them only once per block reference config, instead of once
every single time the blocks are referenced.

This will lead to significant performance improvements in Payload
Configs with a lot of duplicative block references, as permissions are
calculated every time you navigate from page to page.

# Benchmarks

Tested using `pnpm dev benchmark-blocks`.

## Before - ~ 6 seconds


https://github.com/user-attachments/assets/85cac698-3120-414f-91d3-608a404a3a5f


## After - ~ 2 seconds


https://github.com/user-attachments/assets/0c3642f6-6001-41ae-a7cd-f30b24362e9b
2025-02-18 11:16:04 -05:00
Said Akhrarov
74ce8892c4 fix(plugin-seo): add missing supported languages (#11254)
### What?
This PR adds missing languages to `plugin-seo` that are supported in
Payload but were not ported to the plugin.

### Why?
To properly translate the custom keys added by the plugin in all
languages currently supported.

### How?
By adding the missing languages and exporting them for use.

Addresses #11201
2025-02-18 15:40:28 +00:00
Patrik
5817b81289 fix(ui): selection status not updating after toggleAll in useSelection hook (#11218)
### What?

After clicking "Select all" `toggleAll(true)`, manually deselecting an
item does not update the overall selection status.

The bulk actions remain visible, and `selectAll` incorrectly stays as
`AllAvailable`.

### How?

Updated `setSelection()` logic to adjust `selectAll` when deselecting an
item if it was previously set to `AllAvailable`.

This ensures that the selection state updates correctly without altering
the effect logic.

`selectAll` switches to Some when an item is deselected after selecting
all.

Bulk actions now hide correctly if no items are selected.

Fixes #10836
2025-02-18 10:28:54 -05:00
Sasha
847d8d824f feat: add page query parameter for joins (#10998)
Currently, the join field outputs to its result `hasNextPage: boolean`
and have the `limit` query parameter but lacks `page` which can be
useful. This PR adds it.
2025-02-18 16:47:09 +02:00
Jessica Chowdhury
8a2b712287 feat(ui): adds admin.components.listMenuItems option (#11230)
### What?
Adds new option `admin.components.listMenuItems` to allow custom
components to be injected after the existing list controls in the
collection list view.

### Why?
Needed to facilitate import/export plugin.

#### Testing

Use `pnpm dev admin` to see example component and see test added to
`test/admin/e2e/list-view`.


## Update since feature was reverted
The custom list controls and now rendered with no surrounding padding or
border radius.

<img width="596" alt="Screenshot 2025-02-17 at 5 06 44 PM"
src="https://github.com/user-attachments/assets/57209367-5433-4a4c-8797-0f9671da15c8"
/>

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-02-18 09:35:27 -05:00
Sasha
117949b8d9 test: regenerate payload-types.ts for all test suites (#11238)
Regenerates `payload-types.ts` for all test suites.
2025-02-18 00:45:59 +02:00
Sasha
e78500feef test: resolves select type errors (#11235)
This PR fixes all typescript errors inside the `select` test suite.
2025-02-18 00:33:44 +02:00
Alessio Gravili
d49de7bdf8 perf: do not populate globals when calculating permissions, cleanup getEntityPolicies (#11237)
Previously, we forgot to add `depth: 0` to our `findGlobal` call in `getEntityPolicies`. This PR adds `depth: 0` which will be faster.

It also cleans up the `getEntityPolicies` function in general by adding missing types, JSDocs and improving code readability.

This was part of https://github.com/payloadcms/payload/pull/11236 and has been extracted into this separate PR, to make it easier to review
2025-02-17 22:31:27 +00:00
Dan Ribbens
daaaa5f1be feat(ui): add hideFileInputOnCreate and hideRemoveFile to collection upload config (#11217)
### What?

Two new configuration properties added for upload enabled collections.
- *hideFileInputOnCreate* - Set to `true` to prevent the admin UI from
showing file inputs during document creation, useful for programmatic
file generation.
- *hideRemoveFile* - Set to `true` to prevent the admin UI having a way
to remove an existing file while editing.

### Why?

When using file uploads that get created programmatically in
`beforeOperation` hooks or files created using `jobs`, or when
`filesRequiredOnCreate` is false, you may want to use these new flags to
prevent users from interacting with these controls.

### How?

The new properties only impact the admin UI components to dial in the UX
for various use cases.

Screenshot showing that the upload controls are not available on create:

![image](https://github.com/user-attachments/assets/5560b9ac-271d-4ee0-8bcf-6080012ff75f)

Screenshot showing hideRemoveFile has removed the ability to remove the
existing file:

![image](https://github.com/user-attachments/assets/71c562dd-c425-40e6-b980-f65895979885)

Prerequisite for https://github.com/payloadcms/payload/pull/10795
2025-02-17 16:36:38 -05:00
Patrik
ee0ac7f9c0 test: resolves locked-documents type errors (#11223)
This update addresses all TS errors in the e2e & int tests for locked
documents.

- Corrects type mismatches
- Adds type assertions
2025-02-17 15:55:34 -05:00
Alessio Gravili
e6fea1d132 fix: localized fields within block references were not handled properly if any parent is localized (#11207)
The `localized` properly was not stripped out of referenced block fields, if any parent was localized. For normal fields, this is done in sanitizeConfig. As the same referenced block config can be used in both a localized and non-localized config, we are not able to strip it out inside sanitizeConfig by modifying the block config.

Instead, this PR had to bring back tedious logic to handle it everywhere the `field.localized` property is accessed. For backwards-compatibility, we need to keep the existing sanitizeConfig logic. In 4.0, we should remove it to benefit from better test coverage of runtime field.localized handling - for now, this is done for our test suite using the `PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY` flag.
2025-02-17 19:50:32 +00:00
Sasha
749962a1db fix: upload and auth endpoints are mounted for all collections (#11231)
This PR ensures, that collections that don't have `auth: true` don't
mount authentication related endpoints like `/me`, the same for uploads.
Additionally, moves upload-related endpoints to `uploads/endpoints/*`.
2025-02-17 21:11:40 +02:00
Jacob Fletcher
3229b9a4a6 docs: dedicated custom components docs (#10987)
Adds a dedicated "Custom Components" section to the docs.

As users become familiar with building custom components, not all areas
that support customization are well documented. Not only this, but the
current pattern does not allow for deep elaboration on these concepts
without their pages growing to an unmanageable size. Custom components
in general is a large enough topic to merit a standalone section with
subpages. This change will make navigation much more intuitive, help
keep page size down, and provide room to document every single available
custom component with snippets to show exactly how they are typed, etc.

This is a substantial change to the docs, here is the overview: 

- The "Admin > Customizing Components" doc is now located at "Custom
Components > overview"
- The "Admin > Views" doc is now located at "Custom Components > Custom
Views"
- There is a new "Custom Components > Edit View" doc
- There is a new "Custom Components > List View" doc
- The information about root components within the "Admin > Customizing
Components" doc has been moved to a new "Custom Components > Root
Components" doc
- The information about custom providers within the "Admin > Customizing
Components" doc has been moved to a new "Custom Components > Custom
Providers" doc

Similar to the goals of #10743, #10742, and #10741.

Fixes #10872 and initial scaffolding for #10353.

Dependent on #11126.

This change will require the following redirects to be set up:

- `/docs/admin/hooks` → `/docs/admin/react-hooks`
- `/docs/admin/components` → `/docs/custom-components/overview`
- `/docs/admin/views` → `/docs/custom-components/views`
2025-02-17 14:08:40 -05:00
Jacob Fletcher
b80010b1a1 feat: view component types (#11126)
It is currently very difficult to build custom edit and list views or
inject custom components into these views because these views and
components are not explicitly typed. Instances of these components were
not fully type safe as well, i.e. when rendering them via
`RenderServerComponent`, there was little to no type-checking in most
cases.

There is now a 1:1 type match for all views and view components and they
now receive type-checking at render time.

The following types have been newly added and/or improved:

List View:

  - `ListViewClientProps`
  - `ListViewServerProps`
  - `BeforeListClientProps`
  - `BeforeListServerProps`
  - `BeforeListTableClientProps`
  - `BeforeListTableServerProps`
  - `AfterListClientProps`
  - `AfterListServerProps`
  - `AfterListTableClientProps`
  - `AfterListTableServerProps`
  - `ListViewSlotSharedClientProps`

Document View:

  - `DocumentViewClientProps`
  - `DocumentViewServerProps`
  - `SaveButtonClientProps`
  - `SaveButtonServerProps`
  - `SaveDraftButtonClientProps`
  - `SaveDraftButtonServerProps`
  - `PublishButtonClientProps`
  - `PublishButtonServerProps`
  - `PreviewButtonClientProps`
  - `PreviewButtonServerProps`

Root View:

  - `AdminViewClientProps`
  - `AdminViewServerProps`

General:

  - `ViewDescriptionClientProps`
  - `ViewDescriptionServerProps`

A few other changes were made in a non-breaking way:

  - `Column` is now exported from `payload`
  - `ListPreferences` is now exported from `payload`
  - `ListViewSlots` is now exported from `payload`
  - `ListViewClientProps` is now exported from `payload`
- `AdminViewProps` is now an alias of `AdminViewServerProps` (listed
above)
- `ClientSideEditViewProps` is now an alias of `DocumentViewClientProps`
(listed above)
- `ServerSideEditViewProps` is now an alias of `DocumentViewServerProps`
(listed above)
- `ListComponentClientProps` is now an alias of `ListViewClientProps`
(listed above)
- `ListComponentServerProps` is now an alias of `ListViewServerProps`
(listed above)
- `CustomSaveButton` is now marked as deprecated because this is only
relevant to the config (see correct type above)
- `CustomSaveDraftButton` is now marked as deprecated because this is
only relevant to the config (see correct type above)
- `CustomPublishButton` is now marked as deprecated because this is only
relevant to the config (see correct type above)
- `CustomPreviewButton` is now marked as deprecated because this is only
relevant to the config (see correct type above)
 
This PR _does not_ apply these changes to _root_ components, i.e.
`afterNavLinks`. Those will come in a future PR.

Related: #10987.
2025-02-17 14:08:23 -05:00
Sasha
938472bf1f fix: populate is ignored for nested relationships (#11227)
### What?
As described in https://github.com/payloadcms/payload/issues/11209,
previously, the `populate` argument was ignored for nested
relationships.

### Why?
`populate` should work for nested relationships, no matter where they
are in the tree.

### How?
Preserves the `populate` argument in the payload data loader.

Fixes https://github.com/payloadcms/payload/issues/11209
2025-02-17 19:44:21 +02:00
Sasha
64d0217456 templates: allow to pass resource={null} to Media component (#11228)
The `Media` component has an optional property `resource` so we can skip
that property. As in payload `required: false` types are generated like
`media?: Media | string | null`, it also makes sense to allow `null` as
a `resource` value.

Fixes https://github.com/payloadcms/payload/issues/11200
2025-02-17 19:44:10 +02:00
Alessio Gravili
d126c2bf80 chore: move dequal to devDependencies (#11220)
This was accidentally added to test dependencies. Doesn't really make a difference, but for consistency this should be part of devDependencies
2025-02-17 02:42:00 +00:00
Said Akhrarov
b646485388 docs: adds onInit to payload config options (#11069)
This PR adds the `onInit` function to the Payload config options table.
2025-02-16 21:41:43 -05:00
Alessio Gravili
6b9d81a746 fix: ensure leavesFirst option works correctly in traverseFields utility (#11219)
We have to ensure the arguments are handled wherever we push to the callback stack, not when we execute the callback stack.
2025-02-17 02:14:46 +00:00
Sasha
513ba636af fix(db-postgres): ensure countDistinct works correctly and achieve better performance when the query has table joins (#11208)
The fix, added in https://github.com/payloadcms/payload/pull/11096
wasn't sufficient enough. It did handle the case when the same query
path / table was joined twice and caused incorrect `totalDocs`, but it
didn't handle the case when `JOIN` returns more than 1 rows, which 2
added new assertions here check.

Now, we use `COUNT(*)` only if we don't have any joined tables. If we
do, instead of using `SELECT (COUNT DISTINCT id)` which as described in
the previous PR is _very slow_ for large tables, we use the following
query:

```sql
SELECT COUNT(1) OVER() as count -- window function, executes for each row only once
FROM users
LEFT JOIN -- ... here additional rows are added
WHERE -- ...
GROUP BY users.id -- this ensures we're counting only users without additional rows from joins. 
LIMIT 1 -- Since COUNT(1) OVER() executes and resolves before doing LIMIT, we can safely apply LIMIT 1.
```
2025-02-16 14:08:08 +02:00
Sasha
2ae670e0e4 test(db-mongodb): unit test assertion for relationship sanitization inside blockReferences (#11195)
This PR adds a new unit test assertion to existing
https://github.com/payloadcms/payload/blob/main/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.spec.ts
that ensures relationships are sanitized to `ObjectID`s correctly when
saved to the database for relationships inside the new `blockReferences`
https://github.com/payloadcms/payload/pull/10905
2025-02-16 14:07:58 +02:00
Riley Langbein
779f511fbf fix(ui): properly handle singular and plural bulk edit labels (#11198)
Bulk-many components are always using the plural format in their title,
even if only one document has been selected.

This fix checks the selection count and if its greater than 1 it will
show the plural format otherwise it will show the singular format.
2025-02-15 23:25:39 +00:00
annes
35d845cea5 docs: fix typo in readme (#11196) 2025-02-15 22:00:51 +00:00
Alessio Gravili
dc36572fbf feat(richtext-lexical): export INSERT_BLOCK_COMMAND and INSERT_INLINE_BLOCK_COMMAND (#11193)
Lexical checks commands by reference equality. This means that even if you re-define those commands in your own codebase using the same command `type` string, they will be treated as different commands.

If you wanted to dispatch the block creation command in your own codebase (e.g. from a different lexical feature, or any component within the editor), this will not be possible right now. See https://discord.com/channels/967097582721572934/1339557113898340352/1339557113898340352

This PR exports them from `@payloadcms/richtext-lexical/client`
2025-02-14 21:36:00 +00:00
Said Akhrarov
cba5c7bcac fix(ui): hide edit button on deleted relationship options (#11005)
### What?
This PR fixes an issue where a deleted relationship entry would lead to
a runtime error if the user clicked on the edit button in ui due to not
having a `doc` available in `handleServerFunction`.

### Why?
To prevent runtime errors during expected usage.

### How?
By hiding the edit button in entries that have been deleted. This is
done for entries where the user does not have read access already.

Fixes #11004

Before:

[Editing---Post-userdelete--before--Payload.webm](https://github.com/user-attachments/assets/33180eba-9be3-418f-92d2-3bad93e3dfae)

After:

[Editing---Post-userdelete--after--Payload.webm](https://github.com/user-attachments/assets/ba1a736b-3422-4fe0-93ae-7e8e6496d1bd)
2025-02-14 14:45:55 -05:00
Patrik
70db44f964 fix(next): document header padding on tablet sized screens (#11192)
This PR fixes an issue where padding around the `DocumentHeader`
component disappears at the `mid-break` viewport size.

The issue was caused by .doc-header applying padding-left: 0 and
padding-right: 0, which overrode the intended padding from the parent
Gutter component in certain scenarios.
2025-02-14 13:27:10 -05:00
Said Akhrarov
077fb3ab30 fix(ui): respect locale in buildTableState (#11147)
### What?
This PR fixes an issue where the `join` field table was not respecting
the locale selected in the admin ui localizer.

This also introduces an e2e test to the existing suite to catch this
issue.

### Why?
To properly render `join` field table data according to chosen and
configured locales.

### How?
Threading `req.locale` through to the `payload.find` call in
`buildTableState`.

Fixes #11134

Before:

[Editing---Category--join-locales-before-Payload.webm](https://github.com/user-attachments/assets/d77b71bb-f849-4be2-aa96-26dbfedb52d4)

After:

[Editing---Category--join-locales-after-Payload.webm](https://github.com/user-attachments/assets/0d1f7351-adf4-4bad-ac82-0fee67f8b66a)
2025-02-14 12:23:49 -05:00
Jarrod Flesch
b65ae073d2 fix(plugin-multi-tenant): corrects default value for tenantsArrayTenantFieldName (#11189)
Incorrect default value on exported `tenantsArrayField` field. Should
have been `tenant` but was using `tenants`. This affected the
multi-tenant example which uses a custom tenants array field.

You would not notice this issue unless you were using:
```ts
tenantsArrayField: {
  includeDefaultField: false,
}
```

Fixes https://github.com/payloadcms/payload/issues/11125
2025-02-14 10:55:19 -05:00
Germán Jabloñski
480113a4fe docs: remove file extension from import statement in useLexicalComposerContext (#11188)
that import does not exist, it is a typo

reported in
[Discord](https://discord.com/channels/967097582721572934/1328768805450809415)
2025-02-14 15:05:29 +00:00
Sasha
b1734b0d38 fix(ui): hide array field "add" button if admin.readOnly: true is set (#11184)
Fixes https://github.com/payloadcms/payload/issues/11178
2025-02-14 09:06:46 -05:00
Max Malm
84c838cde1 docs: fix importMap.baseDir path (#11076)
Wrong path in docs for `config.admin.importMap.baseDir`

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-02-14 08:48:52 -05:00
Jacob Fletcher
0a3820a487 fix(ui): unable to use browser back navigation after visiting list view (#11172) 2025-02-14 07:47:00 -05:00
Dan Ribbens
dd28959916 fix: join field does not show validation error (#11170)
### What?

Assuming you have a hook in your collection that is looking for certain
conditions to be met related to the join field. The way you would
prevent it is to throw a `new ValidationError()` with errors containing
the path of the field. Previously, the error message for the field would
not show in the admin UI at all.

### Why?

Users need to be able to see any custom error messages for joins field
in the UI so they can address the issue.

### How?

Adds an error class and display the FieldError in the Join field in the
UI component.
2025-02-13 23:09:04 -05:00
Said Akhrarov
12f51bad5f fix(db-mongodb): remove duplicative indexing of timestamps (#11028)
### What?
This PR removes a pair unnecessary calls to `schema.index` against the
timestamp fields. The issue is when a user sets `indexSortableFields` as
this is what will ultimately pass the predicate which then creates
duplicate indexes.

### Why?
These calls are redundant as `index` is [already
passed](https://github.com/payloadcms/payload/blob/main/packages/db-mongodb/src/models/buildSchema.ts#L69)
to the underlying fields base schema options in the process of
formatting and will already be indexed.

These warnings were surfaced after the bump to mongoose to version 8.9.5
as [in 8.9.3 mongoose began throwing these warnings to indicate
duplicative
indexes](https://github.com/Automattic/mongoose/releases/tag/8.9.3).

### How?
By removing these calls and, as a result, silencing the warnings thrown
by mongoose.
2025-02-13 23:07:24 -05:00
Alessio Gravili
4c8cafd6a6 perf: deduplicate blocks used in multiple places using new config.blocks property (#10905)
If you have multiple blocks that are used in multiple places, this can quickly blow up the size of your Payload Config. This will incur a performance hit, as more data is
1.  sent to the client (=> bloated `ClientConfig` and large initial html) and
2. processed on the server (permissions are calculated every single time you navigate to a page - this iterates through all blocks you have defined, even if they're duplicative)

This can be optimized by defining your block **once** in your Payload Config, and just referencing the block slug whenever it's used, instead of passing the entire block config. To do this, the block can be defined in the `blocks` array of the Payload Config. The slug can then be passed to the `blockReferences` array in the Blocks Field - the `blocks` array has to be empty for compatibility reasons.

```ts
import { buildConfig } from 'payload'
import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical'

// Payload Config
const config = buildConfig({
  // Define the block once
  blocks: [
    {
      slug: 'TextBlock',
      fields: [
        {
          name: 'text',
          type: 'text',
        },
      ],
    },
  ],
  collections: [
    {
      slug: 'collection1',
      fields: [
        {
          name: 'content',
          type: 'blocks',
          // Reference the block by slug
          blockReferences: ['TextBlock'],
          blocks: [], // Required to be empty, for compatibility reasons
        },
      ],
    },
     {
      slug: 'collection2',
      fields: [
        {
          name: 'editor',
          type: 'richText',
          editor: lexicalEditor({
            BlocksFeature({
              // Same reference can be reused anywhere, even in the lexical editor, without incurred performance hit
              blocks: ['TextBlock'],
            })
          })
        },
      ],
    },
  ],
})
```

## v4.0 Plans

In 4.0, we will remove the `blockReferences` property, and allow string block references to be passed directly to the blocks `property`. Essentially, we'd remove the `blocks` property and rename `blockReferences` to `blocks`.

The reason we opted to a new property in this PR is to avoid breaking changes. Allowing strings to be passed to the `blocks` property will prevent plugins that iterate through fields / blocks from compiling.

## PR Changes

- Testing: This PR introduces a plugin that automatically converts blocks to block references. This is done in the fields__blocks test suite, to run our existing test suite using block references.

- Block References support: Most changes are similar. Everywhere we iterate through blocks, we have to now do the following:
1. Check if `field.blockReferences` is provided. If so, only iterate through that.
2. Check if the block is an object (= actual block), or string
3. If it's a string, pull the actual block from the Payload Config or from `payload.blocks`.

The exception is config sanitization and block type generations. This PR optimizes them so that each block is only handled once, instead of every time the block is referenced.

## Benchmarks

60 Block fields, each block field having the same 600 Blocks.

### Before:
**Initial HTML:** 195 kB
**Generated types:** takes 11 minutes, 461,209 lines

https://github.com/user-attachments/assets/11d49a4e-5414-4579-8050-e6346e552f56

### After:
**Initial HTML:** 73.6 kB
**Generated types:** takes 2 seconds, 35,810 lines

https://github.com/user-attachments/assets/3eab1a99-6c29-489d-add5-698df67780a3

### After Permissions Optimization (follow-up PR)
Initial HTML: 73.6 kB

https://github.com/user-attachments/assets/a909202e-45a8-4bf6-9a38-8c85813f1312


## Future Plans

1. This PR does not yet deduplicate block references during permissions calculation. We'll optimize that in a separate PR, as this one is already large enough
2. The same optimization can be done to deduplicate fields. One common use-case would be link field groups that may be referenced in multiple entities, outside of blocks. We might explore adding a new `fieldReferences` property, that allows you to reference those same `config.blocks`.
2025-02-14 00:08:20 +00:00
Alessio Gravili
152a9b6adf docs: fix invalid link (#11174)
Links have to be defined in markdown - html a tags won't automatically be converted to links anymore.
2025-02-13 16:46:20 -07:00
lucasbajoua
d47c980509 fix(ui): url encode imageCacheTag for media on dashboard (#11164)
### What?
URL encodes the imageCacheTag query param used to render Media on the
Admin Dashboard

### Why?
The format of the timestamp used as the `imageCacheTag` is causing an
`InvalidQueryStringException` when hosting with Cloudfront + Lambda
(SST/OpenNext)
[See issue](https://github.com/payloadcms/payload/issues/11163)

### How?
Uses `encodeURIComponent` on instances where the `imageCacheTag` is
being formatted for the request URL. (In EditUpload, Thumbnail, and
PreviewSizes)

Fixes #11163

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-02-13 21:42:52 +00:00
Kendell Joseph
7f124cfe93 fix(ui): json schema (#11123)
Fixes https://github.com/payloadcms/payload/issues/10166

[fix: json schema #11123 - Watch
Video](https://www.loom.com/share/0f5199234ad1486f910a39165de837e5)

- Using the same `URI` with the same `schema` will throw an error.
- Using the same `URI` with a different `schema` will throw a warning
(but still work).

If you want to use the same schema on a different field, you need to
define a different URI.
2025-02-13 16:26:57 -05:00
Said Akhrarov
6901b2639d fix(ui): prevent omitting fileSize from non-images (#11146)
### What?
This PR displays file size in upload cards for all upload mimetypes. The
current behavior hides this metric from the user if the file mimetype
does not start with `image`.

### Why?
Showing end-users and editors a file size is universally useful - not
only for images, but for all types of files that can be uploaded via the
upload field.

### How?
By making the predicate that adds this metric less restrictive. Instead
of checking if the mimetype is image-like, it checks if the file size is
truthy.

Before:

![image](https://github.com/user-attachments/assets/949e3be9-6dca-43c3-b2f8-a7e91307e48e)

After:

![image](https://github.com/user-attachments/assets/cb500390-dc64-48e3-a87c-e4ec4d19d019)
2025-02-13 16:05:12 -05:00
Jacob Fletcher
16d75a7c7b feat(ui): refines progress bar animation curve (#11167)
Refines the animation curve used in the new progress bar for route
transitions. Uses an exponential acceleration and decay so that the
indicator progresses quickly at the onset, then gradually decelerates at
it approaches completion. Also caps the progress at ~90%.

Introduced in #9275.
2025-02-13 20:37:30 +00:00
Jacob Fletcher
de68ef4548 fix(ui): adds delay to progress bar for fast networks (#11157)
On fast networks where page transitions are quick, such as local dev in
most cases, the progress bar should not render. This leads to a constant
flashing of the progress bar at the top of the screen and does not
provide any value.

The fix is to add a delay to the initial rendering of the progress bar,
and only show if the transition takes longer than _n_ milliseconds. This
value can be adjusted as needed, but right now is set to 150ms.

Introduced in #9275.
2025-02-13 12:35:41 -05:00
Jacob Fletcher
f4639c418f chore(deps): bumps @monaco-editor/react to v4.7.0 to suppress react 19 warnings (#11161)
The `@monaco-editor/react` package now includes React 19 in its peer
dependencies thanks to
https://github.com/suren-atoyan/monaco-react/pull/651. This package was
also incorrectly listed in `payload` as a regular dependency, but since
it's only used for type imports, it should be listed a dev dependency
instead.
2025-02-13 12:24:53 -05:00
Adit
24da30ab74 docs: add inlineBlock converter example to the converters configuration in Converters.mdx (#11158)
### What?
Added a quick example to showcase how to add a converter for inlineBlocks.

### Why?
This is not easy to figure out in the current version. As per [Discord discussion](https://discord.com/channels/967097582721572934/1338624577990823997)

### How?
Added a very basic 3 lines example to keep the file simple.
2025-02-13 17:01:23 +00:00
Sasha
4be410cc4f test: add types testing for select and joins (#11138)
Adds additional type testing for `select` and `joins` Local API
properties to ensure we don't break those between changes
2025-02-13 18:11:31 +02:00
Jacob Fletcher
cd1117515b refactor(ui): deprecates Link props (#11155)
Deprecates all cases where `Link` could be sent as a prop. This was a
relic from the past, where we attempted to make our UI library
router-agnostic. This was a pipe dream and created more problems than it
solved, for example the logout button was missing this prop, causing it
to render an anchor tag and perform a hard navigation (caught in #9275).

Does so in a non-breaking way, where these props are now optional and
simply unused, as opposed to removing them outright.
2025-02-13 11:10:57 -05:00
Jacob Fletcher
3f550bc0ec feat: route transitions (#9275)
Due to nature of server-side rendering, navigation within the admin
panel can lead to slow page response times. This can lead to the feeling
of an unresponsive app after clicking a link, for example, where the
page remains in a stale state while the server is processing. This is
especially noticeable on slow networks when navigating to data heavy or
process intensive pages.

To alleviate the bad UX that this causes, the user needs immediate
visual indication that _something_ is taking place. This PR renders a
progress bar in the admin panel which is immediately displayed when a
user clicks a link, and incrementally grows in size until the new route
has loaded in.

Inspired by https://github.com/vercel/react-transition-progress.

Old:

https://github.com/user-attachments/assets/1820dad1-3aea-417f-a61d-52244b12dc8d

New:

https://github.com/user-attachments/assets/99f4bb82-61d9-4a4c-9bdf-9e379bbafd31

To tie into the progress bar, you'll need to use Payload's new `Link`
component instead of the one provided by Next.js:

```diff
- import { Link } from 'next/link'
+ import { Link } from '@payloadcms/ui'
```

Here's an example:

```tsx
import { Link } from '@payloadcms/ui'

const MyComponent = () => {
  return (
    <Link href="/somewhere">
      Go Somewhere
    </Link>
  )
}
```

In order to trigger route transitions for a direct router event such as
`router.push`, you'll need to wrap your function calls with the
`startRouteTransition` method provided by the `useRouteTransition` hook.

```ts
'use client'
import React, { useCallback } from 'react'
import { useTransition } from '@payloadcms/ui'
import { useRouter } from 'next/navigation'

const MyComponent: React.FC = () => {
  const router = useRouter()
  const { startRouteTransition } = useRouteTransition()
 
  const redirectSomewhere = useCallback(() => {
    startRouteTransition(() => router.push('/somewhere'))
  }, [startRouteTransition, router])
 
  // ...
}
```

In the future [Next.js might provide native support for
this](https://github.com/vercel/next.js/discussions/41934#discussioncomment-12077414),
and if it does, this implementation can likely be simplified.

Of course there are other ways of achieving this, such as with
[Suspense](https://react.dev/reference/react/Suspense), but they all
come with a different set of caveats. For example with Suspense, you
must provide a fallback component. This means that the user might be
able to immediately navigate to the new page, which is good, but they'd
be presented with a skeleton UI while the other parts of the page stream
in. Not necessarily an improvement to UX as there would be multiple
loading states with this approach.

There are other problems with using Suspense as well. Our default
template, for example, contains the app header and sidebar which are not
rendered within the root layout. This means that they need to stream in
every single time. On fast networks, this would also lead to a
noticeable "blink" unless there is some mechanism by which we can detect
and defer the fallback from ever rendering in such cases. Might still be
worth exploring in the future though.
2025-02-13 09:48:13 -05:00
Germán Jabloñski
706410e693 chore: update codeowners (#11151)
I only remove myself from this file.

I'm getting a lot of notifications that don't significantly change those
directories. I'll keep an eye out, but feel free to assign me as a
reviewer wherever you see fit!
2025-02-13 13:51:28 +00:00
Dan Ribbens
3131dba039 docs: filterAvailableLocales (#11031)
Adding documentation and tests missing in PR #11007

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
Co-authored-by: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com>
2025-02-12 14:48:12 -05:00
Jessica Chowdhury
6bfa66c9ff chore: typo in migrate:fresh command (#11140)
### What?
The migration CLI help says `migration:fresh` is available to use - this
doesn't exist, the command should be `migrate:fresh`.

Closes #10965 & #10967
2025-02-12 16:23:11 +00:00
Germán Jabloñski
6eee787493 chore: add typescript-strict-plugin to the payload package for incremental file-by-file migration [skip lint] (#11133)
### What?

Implement the
[typescript-strict-plugin](https://github.com/allegro/typescript-strict-plugin)
plugin in the payload (core) package.

### Why?

1. One strategy for incremental migration is to enable strictness rules
in tsconfig, fix some errors, and push them without committing the
changes to tsconfig.json. However, this is not feasible for a package as
large as Payload that has over 1000 typescript errors. Until the work is
done, new contributions would undo the work being done.
2. Even if no migration work is done after this PR, this change already
improves the strictness of the package. 89 of the 311 files within the
package already satisfy strict mode. This PR only adds a comment
`@ts-strict-ignore` to files that had at least one compilation error.
This way, the propagation of errors in those files is stopped.
3. New files created in the package are strict by default (this was the
main improvement in version 2 of `typescript-strict-plugin`).

I recommend starting the migration with this package because it is the
one that almost all the others depend on. Once we finish this package,
we can repeat the same strategy on another one, or use the strategy I
mentioned in point 1 if the package is small.

### Note

If you don't see errors in the IDE when you uncomment `//
@ts-strict-ignore`, try restarting the typescript server or VSCode


### How to contribute to the migration ❤️

1. Remove `// @ts-strict-ignore` comments from 1 or more files
2. Fix the pending errors (they should appear in your IDE's intellisense
or when running `cd packages/payload` + `pnpm build:types`
3. Submit your PR!

Important: You don't need to fix everything at once! Furthermore, I
recommend breaking this down into very small PRs to trace potential
issues later if there are any. So if you have 5 minutes, tackle a small
file—every bit counts! 🤗
2025-02-12 11:06:03 -05:00
Hulpoi George-Valentin
30c77d8137 fix(ui): safe call within useEffect teardown (#11135)
`NavProvider` useEffects teardown is trying to set `style` on an element
that may not exist. The original code produces the following error:


![image](https://github.com/user-attachments/assets/11a83fbe-67eb-42a9-bd78-749ea98b67c5)

![image](https://github.com/user-attachments/assets/28ed2534-2387-416b-8191-d68b478161aa)

Therefore, a condition has been added to check if `navRef.current` is
truthy.
2025-02-12 15:30:31 +00:00
Paul
707e85ebcf templates: add new readme for quick start on vercel platform (#11131) 2025-02-12 15:28:59 +00:00
Germán Jabloñski
09ada20ce8 chore(richtext-lexical): fix unchecked indexed access, make richtext-lexical full ts strict (part 5/5) (#11132)
This PR concludes the series to make `richtext-lexical` full strict in
TypeScript 🥳
2025-02-12 12:28:28 +00:00
David Hu
9068bdacae fix(richtext-lexical): add container div to table element to allow horizontal scroll in HTML and JSX converters (#11119)
### What?
Add a `div` wrapper to `table` tag in `TableFeature`

### Why?
This allows for adding horizontal scrolling to the table. We use table
in our blog, however, on mobile, the content is wider than the screen
width, and causes a horizontal scroll of all the content. I attached a
video to show. You can see it by visiting the page on mobile
https://magichour.ai/blog/10-best-ai-video-generators


https://github.com/user-attachments/assets/55778765-697e-426d-ac8a-1b0913adac13


Adding this container div allow me to target the div with a style
```css
.lexical-table-container {
  overflow-x: scroll;
}
```

### How?
![Screenshot 2025-02-11 at 11 26
18 AM](https://github.com/user-attachments/assets/873050b3-79b9-49ec-85ed-297286813577)

I tested this change by manually editing the HTML in our blog to include
the `div` with the overflow style, and it fixes the issue.

Also, verified just adding the `div` did not change anything related to
the rendered output.
2025-02-12 09:10:49 -03:00
Alessio Gravili
155f9f80fe feat: add siblingFields arg to field hooks (#11117)
This PR adds a new `siblingFields` argument to field hooks. This allows
us to dramatically simplify the `lexicalHTML` field, which previously
had to use a complex `findFieldPathAndSiblingFields` function that
deeply traverses the entire `CollectionConfig` just to find the sibling
fields.
2025-02-11 14:22:31 -07:00
Alessio Gravili
2056e9b740 fix(richtext-lexical): reliably install exact lexical version by removing it from peerDeps (#11122)
This will hopefully allow pnpm to reliably install the correct lexical version, as lexical is now solely part of our `dependencies`. Currently, pnpm completely disregards lexical version bumps until the user deletes both the lockfile and their `node_modules` folder.

The downside of this is that pnpm will no longer throw a warning if payload is installed in a project with a mismatching lexical version. However, noone read that warning anyways, and our runtime dependency checker is more reliable.
2025-02-11 21:01:33 +00:00
Paul
44be433d44 templates: add packageManager to website template instead of engines.pnpm (#11121)
Should fix breaking changes issues with pnpm 10
2025-02-11 19:50:40 +00:00
Germán Jabloñski
002e921ede chore(richtext-lexical): improve types of UploadData (#10982)
One step closer to being able to remove `noUncheckedIndexedAccess` in
`packages/richtext-lexical/tsconfig.json`.

I'm introducing UploadData_P4 which is a more precise version of
UploadData. I'm doing it as a different type because there's a chance
it'll be a breaking change for some users.

UploadData is used in many places, but I'm currently replacing it only
in
`packages/richtext-lexical/src/exports/react/components/RichText/converter/converters/upload.tsx`,
because in the other files it's too rooted to other types like
UploadNode.
2025-02-11 14:24:05 -05:00
Alejandro Martinez
3098f35537 docs: explains i18n language changing (#10964)
Elaborate how one is supposed to change the admin panel's language
because it is not initially clear or trivial to someone new and going
through the docs from the start.

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-02-11 14:14:49 -05:00
Alejandro Martinez
d7dee225fc docs: explains i18n installation (#10963)
Make it clearer that you need to install `@payloadcms/translations`. I
think it would help for new people, especially new programmers.

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-02-11 13:53:55 -05:00
Cody Stallings
c31bff7e57 docs: fixes misc grammar and spelling errors (#10996)
Fixed various spelling/grammatical errors found when reading the docs.
2025-02-11 18:37:23 +00:00
Elliot DeNolf
5d199587a3 templates: bump for v3.23.0 (#11115)
🤖 Automated bump of templates for v3.23.0

Triggered by user: @denolfe

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-11 13:25:25 -05:00
Sasha
ececa65c78 fix(next): pre-flight OPTIONS request errors from the graphql endpoint (#11103)
Fixes https://github.com/payloadcms/payload/issues/11101
2025-02-11 13:22:16 -05:00
Germán Jabloñski
7a400a7a79 fix(richtext-lexical): unindent button in toolbar is never active (#11089)
Fixes #11082

In addition to fixing the bug described in that issue, I'm fixing the
problem where when outdenting, indent 0 blocks stay the same. The new
logic verifies that all selected blocks can be outdented.

It remains to be done the same with the tab and shift + tab commands.
2025-02-11 13:21:58 -05:00
Jacob Fletcher
2a0094def7 fix(ui): relationship filterOptions not applied within the list view (#11008)
Fixes #10440. When `filterOptions` are set on a relationship field,
those same filters are not applied to the `Filter` component within the
list view. This is because `filterOptions` is not being thread into the
`RelationshipFilter` component responsible for populating the available
options.

To do this, we first need to be resolve the filter options on the server
as they accept functions. Once resolved, they can be prop-drilled into
the proper component and appended onto the client-side "where" query.

Reliant on #11080.
2025-02-11 13:20:55 -05:00
Elliot DeNolf
48471b7210 chore: tsconfig.base.json reset 2025-02-11 13:02:24 -05:00
Elliot DeNolf
480c6e7c09 chore(release): v3.23.0 [skip ci] 2025-02-11 12:53:51 -05:00
Elliot DeNolf
da77f99df4 fix(payload-cloud): handle socket closures (#11113)
- Port #11015 to handle sockets
- Fix `AccessDenied` errors to properly return 404 in specific scenarios
- Add optional `debug` flag
2025-02-11 12:50:46 -05:00
Paul
ae4a78b298 templates: bump engines pnpm version to support 10 (#11112)
Bumps to support v10 of pnpm in our website templates so installation
doesn't fail on Vercel
2025-02-11 14:46:41 +00:00
Jacob Fletcher
da6511eba9 fix(ui): relationship filter renders stale values when changing fields (#11080)
Fixes #9873. The relationship filter in the "where" builder renders
stale values when switching between fields or adding additional "and"
conditions. This was because the `RelationshipFilter` component was not
responding to changes in the `relationTo` prop and failing to reset
internal state when these events took place.

While it sounds like a simple fix, it was actually quite extensive. The
`RelationshipFilter` component was previously relying on a `useEffect`
that had a callback in its dependencies. This was causing the effect to
run uncontrollably using old references. To avoid this, we use the new
`useEffectEvent` approach which allows the underlying effect to run much
more precisely. Same with the `Condition` component that wraps it. We
now run callbacks directly within event handlers as much as possible,
and rely on `useEffectEvent` _only_ for debounced value changes.

This component was also unnecessarily complex...and still is to some
degree. Previously, it was maintaining two separate refs, one to track
the relationships that have yet to fully load, and another to track the
next pages of each relationship that need to load on the next run. These
have been combined into a single ref that tracks both simultaneously, as
this data is interrelated.

This change also does some much needed housekeeping to the
`WhereBuilder` by improving types, defaulting the operator field, etc.

Related: #11023 and #11032

Unrelated: finds a few more instances where the new `addListFilter`
helper from #11026 could be used. Also removes a few duplicative tests.
2025-02-11 09:45:41 -05:00
Sasha
1f3ccb82d9 docs: update custom endpoints docs, handler does not accept array of functions anymore (#11110)
Fixes https://github.com/payloadcms/payload/issues/11109

Rewrites the description for the `handler` property of the `Endpoint`
type. This function:
* does not have `res` and `next` anymore
* the `handler` property does not accept an array of functions anymore.

Additionally, adds a more meaningful description for the `req` argument.
2025-02-11 13:56:34 +02:00
Said Akhrarov
d6a03eeaba docs: adds options table to payload-wide upload options (#10904)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR adds a table to the [Payload-wide Upload
Options](https://payloadcms.com/docs/upload/overview#payload-wide-upload-options)
section of the docs.

### Why?
To give users more insight into the customization options provided
out-of-the-box with uploads. Previously, these options were not visible
on the docs, forcing users to inspect source code to see how they can
customize their global upload settings. It wasn't clear, for example,
that a `fileSize` limit would not produce a 413 in a response by
default, but would truncate the file contents instead.

### How?
Changes to `docs/upload/overview.mdx`.
2025-02-11 01:19:58 +00:00
Jonathan Bredo
3f80c5993c docs: fixing 3 dead internal links (#11100)
### What?
Dead links located in docs, replaced with functioning links.

### Why?
It broke my [Cursor AI](https://github.com/getcursor/cursor) from
crawling and indexing the docs :(

### How?
Identified broken links by a free online service,
https://www.deadlinkchecker.com/, and fixed all links prefixed with
`https://payloadcms.com/docs`

[Referenced in
Discord.](https://discord.com/channels/967097582721572934/967097582721572937/1338664792717525032)
2025-02-11 01:09:11 +00:00
Paul
c18c58e1fb feat(ui): add timezone support to scheduled publish (#11090)
This PR extends timezone support to scheduled publish UI and collection,
the timezone will be stored on the `input` JSON instead of the
`waitUntil` date field so that we avoid needing a schema migration for
SQL databases.


![image](https://github.com/user-attachments/assets/0cc6522b-1b2f-4608-a592-67e3cdcdb566)

If a timezone is selected then the displayed date in the table will be
formatted for that timezone.

Timezones remain optional here as they can be deselected in which case
the date will behave as normal, rendering and formatting to the user's
local timezone.

For the backend logic that can be left untouched since the underlying
date values are stored in UTC the job runners will always handle this
relative time by default.

Todo:
- [x] add e2e to this drawer too to ensure that dates are rendered as
expected
2025-02-10 19:48:52 -05:00
Paul
36168184b5 fix(ui): incorrectly incrementing version counts if maxPerDoc is set to 0 (#11097)
Fixes https://github.com/payloadcms/payload/issues/9891

We were incorrectly setting max version count to 0 if it was configured
as maxPerDoc `0` due to `Math.min`
2025-02-10 23:28:40 +00:00
Sasha
98fec35368 fix(db-postgres): incorrect pagination results when querying hasMany relationships multiple times (#11096)
Fixes https://github.com/payloadcms/payload/issues/10810

This was caused by using `COUNT(*)` aggregation instead of
`COUNT(DISTINCT table.id)`. However, we want to use `COUNT(*)` because
`COUNT(DISTINCT table.id)` is slow on large tables. Now we fallback to
`COUNT(DISTINCT table.id)` only when `COUNT(*)` cannot work properly.

Example of a query that leads to incorrect `totalDocs`:
```ts
const res = await payload.find({
  collection: 'directors',
  limit: 10,
  where: {
    or: [
      {
        movies: {
          equals: movie2.id,
        },
      },
      {
        movies: {
          equals: movie1.id,
        },
      },
      {
        movies: {
          equals: movie1.id,
        },
      },
    ],
  },
})
```
2025-02-11 01:16:18 +02:00
Jarrod Flesch
fde526e07f fix: set initialValues alongside values during onSuccess (#10825)
### What?
Initial values should be set from the server when `acceptValues` is
true.

### Why?
This is needed since we take the values from the server after a
successful form submission.

### How?
Add `initialValue` into `serverPropsToAccept` when `acceptValues` is
true.

Fixes https://github.com/payloadcms/payload/issues/10820

---------

Co-authored-by: Alessio Gravili <alessio@gravili.de>
2025-02-10 16:49:06 -05:00
James Mikrut
5dadccea39 feat: adds new jobs.shouldAutoRun property (#11092)
Adds a `shouldAutoRun` property to the `jobs` config to be able to have
fine-grained control over if jobs should be run. This is helpful in
cases where you may have many horizontally scaled compute instances, and
only one instance should be responsible for running jobs.
2025-02-10 21:43:20 +00:00
Jarrod Flesch
d2fe9b0807 fix(db-mongodb): ensures same level operators are respected (#11087)
### What?
If you had multiple operator constraints on a single field, the last one
defined would be the only one used.

Example:
```ts
where: {
  id: {
    in: [doc2.id],
    not_in: [], // <-- only respected this operator constraint
  },
}
```

and
```ts
where: {
  id: {
    not_in: [],
    in: [doc2.id], // <-- only respected this operator constraint
  },
}
```

They would yield different results.

### Why?
The results were not merged into an `$and` query inside parseParams.

### How?
Merges the results within an `$and` constraint.

Fixes https://github.com/payloadcms/payload/issues/10944

Supersedes https://github.com/payloadcms/payload/pull/11011
2025-02-10 16:29:08 -05:00
Markus
95ec57575d fix(storage-s3): sockets not closing (#11015)
### What?
Within collections using the `storage-s3` plugins, we eventually start
receiving the following warnings:

`@smithy/node-http-handler:WARN socket usage at capacity=50 and 156
additional requests are enqueued. See
https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/node-configuring-maxsockets.html
or increase socketAcquisitionWarningTimeout=(millis) in the
NodeHttpHandler config.`

Also referenced in this issue: #6382

The
[solution](https://github.com/payloadcms/payload/issues/6382#issuecomment-2325468104)
provided by @denolfe in that issue only delayed the reappearance of the
problem somewhat, but did not resolve it.

### Why?
As far as I understand, in the `staticHandler` of the plugin, when
getting items from storage, and they are currently cached, the cached
results are immediately returned without handling the stream. As per
[this](https://github.com/aws/aws-sdk-js-v3/blob/main/supplemental-docs/CLIENTS.md#nodejs-requesthandler)
entry in the aws-sdk docs, if the streaming response is not read, or
manually destroyed, a socket might not properly close.

### How?
Before returning the cached items, manually destroy the streaming
response to make certain the socket is being properly closed.
Additionally, add an error check to also consume/destroy the streaming
response in case an error occurs, to not leave orphaned sockets.

Fixes #6382
2025-02-10 15:17:30 -05:00
Paul
430ebd42ff feat: add timezone support on date fields (#10896)
Adds support for timezone selection on date fields.

### Summary

New `admin.timezones` config:

```ts
{
  // ...
  admin: {
    // ...
    timezones: {
      supportedTimezones: ({ defaultTimezones }) => [
        ...defaultTimezones,
        { label: '(GMT-6) Monterrey, Nuevo Leon', value: 'America/Monterrey' },
      ],
      defaultTimezone: 'America/Monterrey',
    },
  }
}
```

New `timezone` property on date fields:

```ts
{
  type: 'date',
  name: 'date',
  timezone: true,
}
```

### Configuration

All date fields now accept `timezone: true` to enable this feature,
which will inject a new field into the configuration using the date
field's name to construct the name for the timezone column. So
`publishingDate` will have `publishingDate_tz` as an accompanying
column. This new field is inserted during config sanitisation.

Dates continue to be stored in UTC, this will help maintain dates
without needing a migration and it makes it easier for data to be
manipulated as needed. Mongodb also has a restriction around storing
dates only as UTC.

All timezones are stored by their IANA names so it's compatible with
browser APIs. There is a newly generated type for `SupportedTimezones`
which is reused across fields.

We handle timezone calculations via a new package `@date-fns/tz` which
we will be using in the future for handling timezone aware scheduled
publishing/unpublishing and more.

### UI

Dark mode

![image](https://github.com/user-attachments/assets/fcebdb7f-be01-4382-a1ce-3369f72b4309)

Light mode

![image](https://github.com/user-attachments/assets/dee2f1c6-4d0c-49e9-b6c8-a51a83a5e864)
2025-02-10 15:02:53 -05:00
Jacob Fletcher
3415ba81ac chore: updates CODEOWNERS (#11088) 2025-02-10 14:51:07 -05:00
Germán Jabloñski
fa18923317 fix(richtext-lexical): improve keyboard navigation on DecoratorNodes (#11022)
Fixes #8506



https://github.com/user-attachments/assets/a5e26f18-2557-4f19-bd89-73f246200fa5
2025-02-10 19:22:25 +00:00
James Mikrut
91a0f90649 fix(next): allows relative live preview urls (#11083)
We now properly allow relative live preview URLs which is handy if
you're deploying on a platform like Vercel and do not know what the
preview domain is going to end up being at build time.

This PR also removes some problematic code in the website template which
hard-codes the protocol to `https://` in production even if you're
running locally.

Fixes #11070
2025-02-10 18:20:34 +00:00
Alessio Gravili
b15a7e3c72 chore(richtext-lexical): add test converage for internal links (#11075)
Adds e2e test coverage for creating internal links, ensuring they are saved and that depth+population works.

This test will prevent regression of https://github.com/payloadcms/payload/issues/11062
2025-02-10 16:10:39 +00:00
Patrik
d56de79671 docs: adds usePayloadAPI hook to React Hooks documentation (#11079)
Adds documentation for the `usePayloadAPI` hook to the React Hooks
documentation.

The new section provides details on how the hook works, its parameters,
return values, and example usage.

**Changes:**
- Added `usePayloadAPI` documentation to the React Hooks page.
- Explained its purpose, arguments, and return values.
- Included an example demonstrating how to fetch data and update request
parameters dynamically.

Fixes: #10969
2025-02-10 11:01:45 -05:00
Patrik
87ba7f77aa docs: fix typo in BlockquoteFeature name (#11078)
### What

Before, richText docs were showing a feature name spelt as
`BlockQuoteFeature`.

### How?

 However, the accurate spelling of the feature is `BlockquoteFeature`.
2025-02-10 10:14:32 -05:00
Alessio Gravili
9fb7160c2c fix(richtext-lexical): toggling between internal and custom links does not update fields (#11074)
Fixes https://github.com/payloadcms/payload/issues/11062

In https://github.com/payloadcms/payload/pull/9869 we fixed a bug where `data` passed to lexical fields did not reflect the document data. Our LinkFeature, however, was depending on this incorrect behavior. This PR updates the LinkFeature field conditions to depend on the `siblingData` instead of `data`
2025-02-10 07:05:23 +00:00
Alessio Gravili
c6c65ac842 chore: ensure jest respects PAYLOAD_DATABASE env variable (#11065)
Previously, if the `PAYLOAD_DATABASE` env variable was set to `postgres`, it would still start up the mongo memory db and write the mongo db adapter.
2025-02-08 23:25:00 +00:00
Nathan Clevenger
dc56acbdaf docs: typo in jobs queue workflows (#11063) 2025-02-08 10:17:47 -05:00
Alessio Gravili
6a99677d15 fix: unhelpful "cannot overwrite model once compiled" errors swallowing actual error (#11057)
If an error is thrown during the payload init process, it gets ignored and an unhelpful, meaningless

` ⨯ OverwriteModelError: Cannot overwrite ___ model once compiled.`
 
error is thrown instead. The actual error that caused this will never be logged. This PR fixes this and ensures the actual error is logged.
 
 ## Why did this happen?
 
If an error is thrown during the init process, it is caught and handled by the `src/utilities/routeError.ts` - this helper properly logs the error using pino.
The problem is that pino did not exist, as payload did not finish initializing - it errored during it. So, it tries to initialize payload again before logging the error... which will fail again. If payload failed initializing the first time, it will fail the second time. => No error is logged.

This PR ensures the error is logged using `console.error()` if the originating error was thrown during the payload init process, instead of attempting to initialize it again and again
2025-02-08 07:32:03 +00:00
Alessio Gravili
6d48cf9bbf fix: error when passing functions to array or block fields labels property (#11056)
Fixes https://github.com/payloadcms/payload/issues/11055

Functions passed to array field, block field or block `labels` were not properly handled in the client config, causing those functions to be sent to the client. This leads to a "Functions cannot be passed directly to Client Component" error
2025-02-07 21:52:01 +00:00
Alessio Gravili
d7a7fbf93a feat(richtext-lexical): expose client config to client features (#11054)
This PR exposes the `ClientConfig` as an argument to the lexical `ClientFeature`. This is a requirement for https://github.com/payloadcms/payload/pull/10905, as we need to get the ClientBlocks from the `clientConfig.blocksMap` if they are strings.

## Example

```tsx
export const BlocksFeatureClient = createClientFeature(
  ({ config, featureClientSchemaMap, props, schemaPath }) => { // <= config is the new argument
  	
  	// Return ClientFeature
})
```
2025-02-07 21:22:38 +00:00
Germán Jabloñski
5a5385423e chore(richtext-lexical): fix unchecked indexed access (part 4) (#11048) 2025-02-07 16:24:57 +00:00
Boyan Bratvanov
ac6f4e2c86 docs(plugin-multi-tenant): update tenantsArrayField config options (#11045) 2025-02-07 10:30:20 -05:00
Germán Jabloñski
886bd94fc3 fix(richtext-lexical): fixed the positioning of the button to add columns or rows in tables (#11050)
Fixes #11042



https://github.com/user-attachments/assets/7b51930f-2861-4661-9551-f1952b7a972b
2025-02-07 10:30:06 -05:00
Elliot DeNolf
a80c6b5212 chore(release): v3.22.0 [skip ci] 2025-02-07 09:22:48 -05:00
Dan Ribbens
6f53747040 revert(ui): adds admin.components.listControlsMenu option (#11047)
Reverts payloadcms/payload#10981

In using this feature I think we need to iterate once more before it can
be released.
2025-02-07 09:15:46 -05:00
Jacob Fletcher
b820a75ec5 fix(ui): removing final condition closes where builder (#11032)
When filtering the list view, removing the final condition from the
query closes the "where" builder entirely. This forces the user to
re-open the filter controls and begin adding conditions from the start.
2025-02-07 09:15:18 -05:00
Germán Jabloñski
49d94d53e0 chore: pnpm dev defaults to the _community test suite (#11044)
- `pnpm dev` defaults to the _community test suite
- add a console log indicating which suite is running
2025-02-07 13:10:24 +00:00
Germán Jabloñski
feea444867 chore: find and use an available port in tests (#11043)
You can now run `pnpm dev [test-suite]` even if port 3000 is busy.

I copied the error message as is from what nextjs shows.
2025-02-07 09:45:06 -03:00
Alessio Gravili
257cad71ce chore: fix eslint wasn't running in test dir (#11036)
This PR fixes 2 eslint config issues that prevented it from running in our test dir

- spec files were ignored by the root eslint config. This should have only ignored spec files within our packages, as they are ignored by the respective package tsconfigs
- defining the payload plugin crashed eslint in our test dir, as it was already defined in the root eslint config it was inheriting
2025-02-07 03:54:26 +00:00
Alessio Gravili
04dad9d7a6 chore: fix flaky lexical test (#11035)
The "select decoratorNodes" test was flaky, as it often selected the relationship block node with a relationship to "payload.jpg", instead of the upload node for "payload.jpg", depending on which node loaded first.

This PR ensures it waits for all blocks to be loaded, and updates the selector to specifically target the upload node
2025-02-07 03:24:49 +00:00
Alessio Gravili
098fe10ade chore: deflake joins e2e tests (#11034)
Previously, data created by other tests was also leaking into unrelated tests, causing them to fail. The new reset-db-between-tests logic added by this PR fixes this. 

Additionally, this increases playwright timeouts for CI, and adds a specific timeout override for opening a drawer, as it was incredibly slow in CI
2025-02-07 02:38:38 +00:00
Jessica Chowdhury
7277f17f14 feat(ui): adds admin.components.listControlsMenu option (#10981)
### What?
Adds new option `admin.components.listControlsMenu` to allow custom
components to be injected after the existing list controls in the
collection list view.

### Why?
Needed to facilitate import/export plugin.

#### Preview & Testing

Use `pnpm dev admin` to see example component and see test added to
`test/admin/e2e/list-view`.
<img width="1443" alt="Screenshot 2025-02-04 at 4 59 33 PM"
src="https://github.com/user-attachments/assets/dffe3a4b-5370-4004-86e6-23dabccdac52"
/>

---------

Co-authored-by: Dan Ribbens <DanRibbens@users.noreply.github.com>
2025-02-06 18:24:04 -05:00
Jacob Fletcher
7a73265bd6 fix(ui): clearing value from relationship filter leaves stale query (#11023)
When filtering the list view using conditions on a relationship field,
clearing the value from the field would leave it in the query despite
being removed from the component.
2025-02-06 17:44:32 -05:00
Jarrod Flesch
ec593b453e chore(plugin-multi-tenant): add better defaults for imported components (#11030)
Creates a default variables file to use in exported components.
Extension of https://github.com/payloadcms/payload/pull/10975.
2025-02-06 22:21:49 +00:00
Jarrod Flesch
a63a3d0518 feat(ui): adds filtering config option and implementation for filtering a… (#11007)
Adds the ability to filter what locales should be available per request.

This means that you can determine what locales are visible in the
localizer selection menu at the top of the admin panel. You could do
this per user, or implement a function that scopes these to tenants and
more.

Here is an example function that would scope certain locales to tenants:

**`payload.config.ts`**
```ts
// ... rest of payload config

localization: {
  defaultLocale: 'en',
  locales: ['en', 'es'],
  filterAvailableLocales: async ({ req, locales }) => {
    if (getTenantFromCookie(req.headers, 'text')) {
      try {
        const fullTenant = await req.payload.findByID({
          id: getTenantFromCookie(req.headers, 'text') as string,
          collection: 'tenants',
        })
        if (fullTenant && fullTenant.supportedLocales?.length) {
          return locales.filter((locale) => {
            return fullTenant.supportedLocales?.includes(locale.code as 'en' | 'es')
          })
        }
      } catch (_) {
        // do nothing
      }
    }
    return locales
  },
}
  ```

The filter above assumes you have a field on your tenants collection like so:

```ts
{
  name: 'supportedLocales',
  type: 'select',
  hasMany: true,
  options: [
    {
      label: 'English',
      value: 'en',
    },
    {
      label: 'Spanish',
      value: 'es',
    },
  ],
}
```
2025-02-06 16:57:59 -05:00
Sasha
57143b37d0 fix(db-postgres): ensure globals have createdAt, updatedAt and globalType fields (#10938)
Previously, data for globals was inconsistent across database adapters.
In Postgres, globals didn't store correct `createdAt`, `updatedAt`
fields and the `updateGlobal` lacked the `globalType` field. This PR
solves that without introducing schema changes.
2025-02-06 23:48:59 +02:00
Sasha
3ad56cd86f fix(db-postgres): select hasMany: true with autosave doesn't work properly (#11012)
Previously, select fields with `hasMany: true` didn't save properly in
Postgres on autosave.
2025-02-06 23:47:53 +02:00
Jacob Fletcher
05e6f3326b test: addListFilter helper (#11026)
Adds a new `addListFilter` e2e helper. This will help to standardize
this common functionality across all tests that require filtering list
tables and help reduce the overall lines of code within each test file.
2025-02-06 16:17:27 -05:00
Alessio Gravili
8b6ba625b8 refactor: do not use description functions for generated types JSDocs (#11027)
In https://github.com/payloadcms/payload/pull/9917 we automatically added `admin.description` as JSDocs to our generated types.

If a function was passed as a description, this could have created unnecessary noise in the generated types, as the output of the description function may differ depending on where and when it's executed.

Example:

```ts
description: () => {
  return `Current date: ${new Date().toString()}`
}
```

This PR disabled evaluating description functions for JSDocs generation
2025-02-06 21:16:44 +00:00
Alessio Gravili
2b76a0484c fix(richtext-lexical): duplicative error paths in validation (#11025) 2025-02-06 21:00:25 +00:00
Alessio Gravili
66318697dd chore: fix lexical tests that are failing on main branch (#11024) 2025-02-06 20:28:30 +00:00
Jacob Fletcher
8940726601 fix(ui): relationship filter clearing on blur (#11021)
When using the filter controls in the list view on a relationship field,
the select options would clear after clicking outside of the component
then never repopulate. This caused the component to remain in an
unusable state, where no options would appear unless the filter is
completely removed and re-added. The reason for this is that the
`react-select` component fires an `onInputChange` event on blur, and the
handler that is subscribed to this event was unknowingly clearing the
options.

This PR also renames the various filter components, i.e.
`RelationshipField` -> `RelationshipFilter`. This improves semantics and
dedupes their names from the actual field components.

This bug was first introduced in this PR: #10553
2025-02-06 15:27:34 -05:00
Alessio Gravili
ae32c555ac fix(richtext-lexical): ensure sub-fields have access to full document data in form state (#9869)
Fixes https://github.com/payloadcms/payload/issues/10940

This PR does the following:
- adds a `useDocumentForm` hook to access the document Form. Useful if
you are within a sub-Form
- ensure the `data` property passed to field conditions, read access
control, validation and filterOptions is always the top-level document
data. Previously, for fields within lexical blocks/links/upload, this
incorrectly was the lexical block-level data.
- adds a `blockData` property to hooks, field conditions,
read/update/create field access control, validation and filterOptions
for all fields. This allows you to access the data of the nearest parent
block, which is especially useful for lexical sub-fields. Users that
were previously depending on the incorrect behavior of the `data`
property in order to access the data of the lexical block can now switch
to the new `blockData` property
2025-02-06 13:49:17 -05:00
Alessio Gravili
8ed410456c fix(ui): improve useIgnoredEffect hook (#10961)
The `useIgnoredEffect` hook is useful in firing an effect only when a _subset_ of dependencies change, despite subscribing to many dependencies. But the previous implementation of `useIgnoredEffect` had a few problems:

- The effect did not receive the updated values of `ignoredDeps` - thus, `useIgnoredEffect` pretty much worked the same way as using `useEffect` and omitting said dependencies from the dependency array. This caused the `ignoredDeps` values to be stale.
- It compared objects by value instead of reference, which is slower and behaves differently than `useEffect` itself.
- Edge cases where the effect does not run even though the dependencies have changed. E.g. if an `ignoredDep` has value `null` and a `dep` changes its value from _something_ to `null`, the effect incorrectly does **not** run, as the current logic detects that said value is part of `ignoredDeps` => no `dep` actually changed.

This PR replaces the `useIgnoredEffect` hook with a new pattern which to combine `useEffect` with a new `useEffectEvent` hook as described here: https://react.dev/learn/separating-events-from-effects#extracting-non-reactive-logic-out-of-effects. While this is not available in React 19 stable, there is a polyfill available that's already used in several big projects (e.g. react-spectrum and bluesky).
2025-02-06 11:37:49 -07:00
Germán Jabloñski
824f9a7f4d chore(cpa): add ts strict mode (#10914) 2025-02-06 12:02:38 -05:00
Jarrod Flesch
f25acb801c fix(plugin-multi-tenant): correctly set doc default value on load (#11018)
When navigating from the list view, with no tenant selected, the
document would load and set the hidden tenant field to the first tenant
option.

This was caused by incorrect logic inside the TenantField useEffect that
sets the value on the field upon load.
2025-02-06 16:24:06 +00:00
Germán Jabloñski
5f58daffd0 chore(richtext-lexical): fix unchecked indexed access (part 3) (#11014)
I start to list the PRs because there may be a few.

1. https://github.com/payloadcms/payload/pull/10982
2. https://github.com/payloadcms/payload/pull/11013
2025-02-06 15:44:02 +00:00
Germán Jabloñski
e413e1df1c chore(richtext-lexical): fix unchecked indexed acess in lexical blocks feature (#11013)
This PR is part of the process of fixing `noUncheckedIndexedAccess` in
richtext-lexical.
2025-02-06 14:07:41 +00:00
Dan Ribbens
bdbb99972c fix(ui): allow schedule publish to be accessed without changes (#10999)
### What?
Using the versions drafts feature and scheduling publish jobs, the UI
does not allow you to open the schedule publish drawer when the document
has been published already.

### Why?
Because of this you cannot schedule unpublish, unless as a user you
modify a form field as a workaround before clicking the publish submenu.

### How?
This change extends the Button props to include subMenuDisableOverride
allowing the schedule publish submenu to still be used on even when the
form is not modified.

Before: 

![image](https://github.com/user-attachments/assets/a69f2e39-d74e-476c-9744-2b8523e2b831)


With changes:

![Animation](https://github.com/user-attachments/assets/0a13fe33-974c-402b-8464-6ef2cb397d86)
2025-02-06 06:58:43 -05:00
Simon Vreman
e29ac523d3 fix(ui): apply cacheTags upload config property to other admin panel image components (#10801)
In https://github.com/payloadcms/payload/pull/10319, the `cacheTags`
property was added to the image config. This achieves the goal as
described, however, there are still other places where this issue
occurs, which should be handled in the same way. This PR aims to apply
it to those instances.
2025-02-06 06:04:03 -05:00
Tobias Odendahl
d8cfdc7bcb feat(ui): improve hasMany TextField UX (#10976)
### What?

This updates the UX of `TextFields` with `hasMany: true` by:
- Removing the dropdown menu and its indicator
- Removing the ClearIndicator
- Making text items directly editable

### Why?
- The dropdown didn’t enhance usability.
- The ClearIndicator removed all values at once with no way to undo,
risking accidental data loss. Backspace still allows quick and
intentional clearing.
- Previously, text items could only be removed and re-added, but not
edited inline. Allowing inline editing improves the editing experience.

### How?


https://github.com/user-attachments/assets/02e8cc26-7faf-4444-baa1-39ce2b4547fa
2025-02-06 06:02:55 -05:00
Jacob Fletcher
694c76d51a test: cleans up fields-relationship test suite (#11003)
The `fields-relationship` test suite is disorganized to the point of
being unusable. This makes it very difficult to digest at a high level
and add new tests.

This PR cleans it up in the following ways:

- Moves collection configs to their own standalone files
- Moves the seed function to its own file
- Consolidates collection slugs in their own file
- Uses generated types instead of defining them statically
- Wraps the `filterOptions` e2e tests within a describe block

Related, there are three distinct test suites where we manage
relationships: `relationships`, `fields-relationship`, and `fields >
relationships`. In the future we ought to consolidate at least two of
these. IMO the `fields > relationship` suite should remain in place for
general _component level_ UI tests for the field itself, whereas the
other suite could run the integration tests and test the more complex UI
patterns that exist outside of the field component.
2025-02-05 17:03:35 -05:00
Alessio Gravili
09721d4c20 fix(next): viewing modified-only diff view containing localized arrays throws error (#11006)
Fixes https://github.com/payloadcms/payload/issues/11002

`buildVersionFields` was adding `null` version fields to the version fields array. When RenderVersionFieldsToDiff tried to render those, it threw an error.

This PR ensures no `null` fields are added, as `RenderVersionFieldsToDiff` can't process them. That way, those fields are properly skipped, which is the intent of `modifiedOnly`
2025-02-05 21:42:38 +00:00
Elliot DeNolf
834fdde088 chore(release): v3.21.0 [skip ci] 2025-02-05 14:15:51 -05:00
James Mikrut
45913e41f1 fix(richtext-lexical): removes css from jsx converter (#10997)
Our new Lexical -> JSX converter is great, but right now it can only be
used in environments that support CSS importing / bundling.

It was only that way because of a single import file which can be
removed and inlined, therefore, improving the versatility of the JSX
converter and making it more usable in a wider variety of runtimes.

---------

Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
2025-02-05 14:03:41 -05:00
Paul
42da87b6e9 fix(plugin-search): deleting docs even when there's a published version (#10993)
Fixes https://github.com/payloadcms/payload/issues/9770

If you had a published document but then created a new draft it would
delete the search doc, this PR adds an additional find to check if an
existing published doc exists before deleting the search doc.

Also adds a few jsdocs to plugin config
2025-02-05 10:14:17 -05:00
Jarrod Flesch
2a1ddf1e89 fix(plugin-multi-tenant): incorrect tenant selection with postgres (#10992)
### What
1. List view not working when clearing tenant selection (you would see a
NaN error)
2. Tenant selector would reset to the first option when loading a
document

### Why
1. Using parseFloat on the _ALL_ selection option
2. A was mismatch in ID types was causing the selector to never find a
matching option, thus resetting it to the first option

### How
1. Check if cookie isNumber before parsing
2. Do not cast select option values to string anymore

Fixes https://github.com/payloadcms/payload/issues/9821
Fixes https://github.com/payloadcms/payload/issues/10980
2025-02-05 09:56:27 -05:00
Elliot DeNolf
8af8befbd4 ci: increase closed issue lock for inactivity to 7 days 2025-02-05 09:15:58 -05:00
James Mikrut
2118c6c47f feat: exposes helpful args to ts schema gen (#10984)
You can currently extend Payload's type generation if you provide
additional JSON schema definitions yourself.

But, Payload has helpful functions like `fieldsToJSONSchema` which would
be nice to easily re-use.

The only issue is that the `fieldsToJSONSchema` requires arguments which
are difficult to access from the context of plugins, etc. They should
really be provided at runtime to the `config.typescript.schema`
functions.

This PR does exactly that. Adds more args to the `schema` extension
point to make utility functions easier to re-use.
2025-02-04 20:12:07 -05:00
Jacob Fletcher
a07fd9eba3 docs: fixes dynamic, fully qualified live preview url args (#10985)
The snippet for generating a dynamic, fully qualified live preview url
was wrong. It was indicating there were two arguments passed to that
function, when in fact there is only one.
2025-02-04 16:57:16 -05:00
Jarrod Flesch
ea9abfdef3 fix: allow public errors to thread through on response (#10419)
### What?
When using `throw new APIResponse("Custom error message", 500, null,
true)` the error message is being replaced with the standard "Something
went wrong" message.

### Why?
We are not checking if the 4th argument (`isPublic`) is false before
masquerading the error message.

### How?
Adds a check for `!err.isPublic` before adjusting the outgoing message.
2025-02-04 18:10:40 +00:00
Elliot DeNolf
b671fd5a6d templates: set pnpm engines to version 9 (#10979)
pnpm v10 + sharp is having issues. Setting to v9 for now.
2025-02-04 10:49:33 -05:00
Boyan Bratvanov
ae0736b738 examples: multi-tenant seed script, readme and other improvements (#10702) 2025-02-04 09:09:26 -05:00
Tylan Davis
1a68fa14bb docs: correct broken NPM badge images on plugin documentation (#10959)
### What?
Fixes broken NPM badge images/links on plugin documentation pages.

### Why?
They were not properly formatted and did not work.

### How?
Corrects the formatting.

Before: https://payloadcms.com/docs/plugins/nested-docs
After:
https://payloadcms.com/docs/dynamic/plugins/nested-docs?branch=docs/npm-badges
2025-02-03 22:45:10 +00:00
Said Akhrarov
b33749905d test: admin list view custom components (#10956)
### What?
This PR adds tests for custom list view components to the existing suite
in `admin/e2e/list-view/`. Custom components are already tested in the
document-level counterpart, and should be tested here as well.

### Why?
Previously, there were no tests for these list view components.
Refactors, features, or changes that impact the importMap, default list
view, etc., could affect how these components get rendered. It's safer
to have tests in place to catch this as custom list view components, in
general, are used quite often.

### How?
This PR adds 5 simple tests that check for the rendering of the
following list view components:
- `BeforeList`
- `BeforeListTable`
- `UI Field Cell`
- `AfterList`
- `AfterListTable`
2025-02-03 16:18:26 -05:00
Jessica Chowdhury
0f85a6e0cc fix(plugin-search): generates full docURL with basePath from next config (#10910)
Fixes #10878. The Search Plugin displays a link within the search
results collection that points to the underlying document that is
related to that result. The href used, however, was not accounting for
any `basePath` provided to the `next.config.js`, leading to a 404 if
using a custom base path. The fix is to use the `Link` component from
`next/link` instead of an anchor tag directly. This will automatically
inject the the base path into the href before rendering it.

This PR also brings back the `CopyToClipboard` component. This makes it
easy for the user to copy the href instead of navigating to it.

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
2025-02-03 15:17:07 -05:00
Jacob Fletcher
177127141e chore(plugin-search): improves types (#10955)
There were a number of areas within the Search Plugin where typings
could have been improved, namely:
- The `customProps` sent to the `ReindexButton`. This now uses the
`satisfies` keyword to ensure type strictness.
- The `collectionLabels` prop sent to the `ReindexButtonClient`
component. This is now standardized behind a new
`ResolvedCollectionLabels` type to closely reflect `CollectionLabels`.
This was also converted from unnecessarily invoking a function to being
a basic object.
- The `locale` type sent through `SyncDocArgs`. This now uses
`Locale['code']` from Payload.
2025-02-03 19:53:04 +00:00
Steve Kuznetsov
0a1cc6adcb templates: use typed functions in website template seed endpoint (#10420)
`JSON.parse(JSON.stringify().replace())` is easy to make mistakes with and since we have TypeScript data objects already for the data we're seeding it's pretty easy to just factor these as functions, making their dependencies explicit.
2025-02-03 12:40:22 -07:00
Jacob Fletcher
4a4e90a170 chore(plugin-search): deprecates apiBasePath from config (#10953)
Continuation of #10632. The `apiBasePath` property in the Search Plugin
config is unnecessary. This plugin reads directly from the Payload
config for this property. Exposing it to the plugin's config was likely
a mistake during sanitization before passing it through to the remaining
files. This property was added to resolve the types, but as result,
exposed it to the config unnecessarily. This PR marks this property with
the deprecated flag to prevent breaking changes.
2025-02-03 19:06:05 +00:00
Alessio Gravili
136c90c725 fix(richtext-lexical): link drawer has no fields if parent document create access control is false (#10954)
Previously, the lexical link drawer did not display any fields if the
`create` permission was false, even though the `update` permission was
true.

The issue was a faulty permission check in `RenderFields` that did not
check the top-level permission operation keys for truthiness. It only
checked if the `permissions` variable itself was `true`, or if the
sub-fields had `create` / `update` permissions set to `true`.
2025-02-03 19:02:40 +00:00
Suphon T.
6353cf8bbe fix(plugin-search): gets api route from useConfig (#10632)
This fixes #10631.

Originally the api basepath for the reindex button is resolved during
plugin initialization. Looks like this happens before payload overrides
the config with the `basePath `from the next config.
I've changed it so that it uses the `useConfig` hook, and manually
tested that it works.

![CleanShot 2568-01-17 at 16 03
16@2x](https://github.com/user-attachments/assets/c931577b-2717-4635-b5c6-17aa1b4eb734)
2025-02-03 18:14:21 +00:00
Alessio Gravili
109de8cdb3 chore(deps): bump packages used to build payload (#10950)
Bumps all babel/esbuild/swc/react compiler packages
2025-02-03 16:53:42 +00:00
Alexander Cato
8ace0cab33 docs: correct grammar and improve clarity on preventing-abuse.mdx (#10937)
## What
Refactored the explanation of complexity limits in the
⁠preventing-abuse.mdx documentation to correct grammar and improve
clarity.

## Why
- Grammar fix: The original sentence omitted the preposition "to" ("way
specify" → "way to specify").
- Readability: The long, compound sentence was difficult to parse at a
glance.
- Concept separation: Merging two ideas (defining limits and explaining
scoring) confused the workflow.

## How
- Added the missing "to" to ensure grammatical correctness.
- Split the sentence into two parts:
  1. Introduces the purpose of complexity limits.
  2. Explains how complexity scores enforce these limits.
- Preserved technical accuracy while simplifying the flow.
2025-02-02 19:07:52 -07:00
Marwin Hormiz
58666fbdef examples: added missing sharp dependency to the remix website package (#10931)
When the sharp module is not added to the website package, you get a
reference error when trying to start a production build. This is solved
by just installing the sharp module.

Solves #10929

Co-authored-by: Marwin Hormiz <marwinhormiz@duobit.se>
2025-02-02 21:46:55 +02:00
Jacob Fletcher
2f787a9126 chore(deps): bumps @faceless-ui/window-info to v3.0.1 and @faceless-ui/scroll-info to 2.0.0 (#10913)
Bumps `@faceless-ui/window-info` to v3.0.1` and
`@faceless-ui/scroll-info` to v2.0.0. This gets them both off beta
versions and includes React 19 stable in their peer deps.

The `@faceless-ui/modal` package, however, has yet to be bumped. This
package is waiting on https://github.com/faceless-ui/modal/issues/63 to
be resolved in order to fully deprecate
[`body-scroll-lock`](https://github.com/willmcpo/body-scroll-lock)
before bumping to stable.
2025-01-31 17:39:04 +00:00
Sasha
68a7de2610 fix(db-postgres): select hasMany inside arrays and blocks with versions (#10829)
Fixes https://github.com/payloadcms/payload/issues/10780

Previously, with enabled versions, nested select `hasMany: true` fields
weren't working with SQL database adapters. This was due to wrongly
passed `parent` to select rows data because we store arrays and blocks
in versions a bit differently, using both, `id` and `_uuid` (which
contains the normal Object ID) columns. And unlike with non versions
`_uuid` column isn't actually applicable here as it's not unique, thus
we need to save blocks/arrays first and then map their ObjectIDs to
generated by the database IDs and use them for select fields `parent`
data
2025-01-31 18:26:04 +02:00
Sasha
e1dcb9594c fix(db-postgres): write operations on polymorphic joined collections throw error (#10854)
Fixes https://github.com/payloadcms/payload/issues/10845
See
https://github.com/payloadcms/payload/issues/10845#issuecomment-2620201486
2025-01-31 18:25:47 +02:00
Franco D'Agostino
2043b4a6ea templates: add @ts-ignore in seed to allow initial build on vercel (#10889)
### What?
Add @ts-ignore in seed to allow initial build on vercel

### Why?
The 1-click setup for the vercel-website template doesn't work because
the initial build fails on vercel

### How?
Added some ts-ignore, similarly to the main payload repo
2025-01-30 16:39:10 -07:00
Alessio Gravili
35e5be8558 fix(ui): client should add back default values for valid and passesCondition form field properties (#10709)
As a result of #9388, the `valid` and `passesCondition` properties no
longer appear in form state. This leads to breaking logic if you were
previously relying on these properties to have explicit values. To fix
this, we simply perform the inverse on these properties before accepting
them into client side form state. In the next major release, we can
accept form state as it is received and instruct users to modify their
logic as needed.

Also comes with a small perf optimization, by keeping the old object
reference of fields if they did not change when server form state comes
back
2025-01-30 21:21:31 +00:00
Jarrod Flesch
398589397e fix(ui): revert unrelated code (#10897)
### What?
Reverts mixed code written for #10825 that accidentally made it into
#10888
2025-01-30 15:58:03 -05:00
Jacob Fletcher
c7ad46c2ac chore(deps): deprecates body-scroll-lock 2025-01-30 15:32:57 -05:00
Jacob Fletcher
8a79e59855 chore(templates): improves and simplifies draft preview (#10895)
Similar to #10876. There were a number of things wrong or in need of
improvement with the Draft Preview implementation of the Website
Template, namely:
- The preview secret was missing entirely, with pointless logic was
written to throw an error if it missing in the search params as opposed
to not matching the environment secret. This will ensure that only admin
users, not _any_ user, can enter into preview mode.
- The preview endpoint was unnecessarily querying the database for a
matching document as opposed to letting the underlying page itself 404
as needed, and it was also throwing an inaccurate error message. The
preview route already checks that the path is relative, so there is no
security risk of redirecting to another domain.
- The `/next/exit-preview` route was duplicated twice.
- The logic to format search params in the preview URL was unnecessarily
complex.
2025-01-30 15:01:18 -05:00
David Murdoch
ebb51731f6 templates: remove unknown CSS values (#10891)
* set font-size to unset
* set font-weight to unset

### What?

Change CSS values in global.css files in 3 examples

### Why?

Apparently, the CSS value of `auto` does not actually exist in CSS for
`font-size` and `font-weight`
[mdn](https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#syntax)
.
[Stylelint](https://stylelint.io/user-guide/rules/declaration-property-value-no-unknown/)
errors made me aware of this. That rule's description is not specific to
`font-size` and `font-weight`.

This is how it looked in the terminal:

```
src/app/(frontend)/globals.css
  12:18  ✖  Unexpected unknown value "auto" for property "font-weight"  declaration-property-value-no-unknown
  13:16  ✖  Unexpected unknown value "auto" for property "font-size"    declaration-property-value-no-unknown
```

### Fixes:

Change `auto` to `unset` since it uses `initial` styles unless the
heading CSS values have been changed by a parent html tag. I'm guessing
this was reset due to tailwind interrupting this somehow.
2025-01-30 14:54:20 -05:00
Jarrod Flesch
be790a9de2 feat(plugin-multi-tenant): allow opting out of tenant access control merge (#10888)
### What?
In some cases you may want to opt out of using the default access
control that this plugin provides on the tenants collection.

### Why?
Other collections are able to opt out of this already, but the tenants
collection specifically was not configured with an opt out capability.

### How?
Adds new property to the plugin config: `useTenantsCollectionAccess`.
Setting this to `false` allows users to opt out and write their own
access control functions without the plugin merging in its own
constraints for the tenant collection.

Fixes https://github.com/payloadcms/payload/issues/10882
2025-01-30 14:49:19 -05:00
Alessio Gravili
85c0842444 fix(ui): error in version view if document contains localized arrays or blocks (#10893)
Fixes https://github.com/payloadcms/payload/issues/10884
2025-01-30 19:45:47 +00:00
Jacob Fletcher
2b9ee62fc0 chore(examples): misc improvements to the draft preview example (#10876)
There were a number of things wrong or could have been improved with the
[Draft Preview
Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview),
namely:

- The package.json was missing `"type": "modue"` which would throw ESM
related import errors on startup
- The preview secret was missing entirely, with pointless logic was
written to throw an error if it missing in the search params as opposed
to not matching the environment secret
- The `/next/exit-preview` route was duplicated twice
- The preview endpoint was unnecessarily querying the database for a
matching document as opposed to letting the underlying page itself 404
as needed, and it was also throwing an inaccurate error message

Some less critical changes were:
- The page query was missing the `depth` and `limit` parameters which is
best practice to optimize performance
- The logic to format search params in the preview URL was unnecessarily
complex
- Utilities like `generatePreviewPath` and `getGlobals` were
unnecessarily obfuscating simple functions
- The `/preview` and `/exit-preview` routes were unecessarily nested
within a `/next` page segment
- Payload types weren't aliased
2025-01-29 23:14:08 -05:00
Pavel B.
8f27f85023 docs: fix typo on overview.mdx (#10877)
Remove repeated `developers` word.

### What?
There was a typo on the plugins overview page, where `developers
developers` was used twice in a row. Mb that was a quote from Steve
Balmer idk.

### Why?
Docs should be pristine.

### How?
Removed the word.
2025-01-29 19:20:17 -07:00
Jacob Fletcher
d7c3b4e17a docs: admin preview and draft preview (#10875)
Thoroughly documents the `admin.preview` feature. Previously, this
information was briefly mentioned in two distinct places, within the
collections config and again within the globals config. This led to
discrepancies over time and was inadequate at describing this feature,
such as having a lack of concrete code examples especially as it relates
to _draft preview_. There has also been confusion between this and Live
Preview.

Now, there is a dedicated page at `/admin/preview` which centralizes
this information into a single document. It also specifically documents
how to achieve _draft preview_ and includes code snippets. This way, we
no longer have to rely solely on the [Draft Preview
Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview)
for this.

Related: #10798
2025-01-29 18:17:23 -05:00
Amelia
7d429f8b65 feat: adds auto resize feature to textarea (#10786)
This PR introduces an auto resize feature for the `textarea` field. 
By default Payload `textarea` field will dynamically [adjust its height
based on its
content](https://github.com/payloadcms/payload/pull/10786#discussion_r1928961885).

---------

Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
2025-01-29 21:40:39 +00:00
Robert Clancy (Robbo)
9638dbe52b fix(plugin-multi-tenant): fixed hardcoded user tenants field (#10782)
### What?
When using custom slugs and field names the tenancy field added to the
users would still attempt to use `tenants` and fail.

### Why?
The tenant/tenancy are hardcoded in `tenantsArrayField()`

### How?
Added the same args that are used in `tenantsField()` for the field
names and relation.
2025-01-29 13:27:00 -05:00
Sasha
2f66bdc2dc fix(ui): create-first-user crashes when users collection has join field (#10871)
Fixes https://github.com/payloadcms/payload/issues/10870
Now we hide join fields from the `/create-first-user` view since they're
not meaningful there.
2025-01-29 19:52:22 +02:00
Elliot DeNolf
5bd17cc111 chore(release): v3.20.0 [skip ci] 2025-01-29 10:41:55 -05:00
Germán Jabloñski
0e5ff246b2 fix(richtext-lexical): preserve selection in Firefox when using LexicalMenu (#10867)
Fixes #10724

The selection is never touched in an `editor.read`, but BEFORE starting
an `editor.update` it is synced with `window.selection`. Firefox for
some reason loses the editor selection, so on the next update the
selection is null.

For reference, there was a brief discussion on the Lexical Discord
server:
https://discord.com/channels/953974421008293909/1333916489870348309
2025-01-29 15:18:24 +00:00
Sasha
3094c92ef3 templates: fix compatibility with pnpm 10 (#10830)
Fixes https://github.com/payloadcms/payload/issues/10813
In pnpm 10 (which isn't "latest" yet), according to the [list of
breaking changes](https://github.com/orgs/pnpm/discussions/8945):
> Lifecycle scripts of dependencies are not executed during installation
by default! This is a breaking change aimed at increasing security. In
order to allow lifecycle scripts of specific dependencies, they should
be listed in the pnpm.onlyBuiltDependencies field of package.json

The sharp package uses a script to install native binaries and so our
templates don't run out of the box with pnpm 10.
2025-01-29 15:58:10 +02:00
Elliot DeNolf
c08f012211 chore(cpa): re-pin template versions (#10857)
Pin create-payload-app to pull latest release versions of templates.
2025-01-29 08:55:29 -05:00
Seno
a47139acfa docs: add missing full stop, fix SlateNodeConverter import (#10860)
- Adding full stop to match other words
- In `@payloadcms/richtext-lexical` – `v3.19.0` SlateNodeConverter is
not imported from `@payloadcms/richtext-lexical/migrate` but rather from
`@payloadcms/richtext-lexical`
2025-01-28 22:07:49 -07:00
Alessio Gravili
219a369603 templates: fix website template not building (#10858)
After our 3.20.0 release, we can remove the `as any` assertion again.

Fixes https://github.com/payloadcms/payload/issues/10840
2025-01-29 04:39:26 +00:00
Germán Jabloñski
c75c6ce6c9 chore(templates): update missing changes in vercel website template (#10827)
This PR migrates some changes that had been made to the website template
and had not been ported to the website template with vercel.

Ideally, so that this does not happen again in the future and we do not
have to do this manually, we could have a script in CI.
2025-01-29 03:39:47 +00:00
Germán Jabloñski
52f86c7780 chore(templates): fix eslint errors in vercel templates (#10768) 2025-01-29 03:14:41 +00:00
Alessio Gravili
c562fbfa94 feat(ui): allows customizing version diff components, render versions ui on the server (#10815)
This PR moves the logic for rendering diff field components in the
version comparison view from the client to the server.

This allows us to expose more customization options to the server-side
Payload Config. For example, users can now pass their own diff
components for fields - even including RSCs.

This PR also cleans up the version view types

Implements the following from
https://github.com/payloadcms/payload/discussions/4197:
- allow for customization of diff components
- more control over versions screens in general

TODO:
- [x] Bring getFieldPaths fixes into core
- [x] Cleanup and test with scrutiny. Ensure all field types display
their diffs correctly
- [x] Review public API for overriding field types, add docs
- [x] Add e2e test for new public API
2025-01-28 22:17:24 +00:00
Tsemach Hadad
33ac13df28 feat(ui): toggle showing only modified fields in version diff view (#10807)
## Description

As an author reviewing the versions I have for a document , I would like
to the ability to focus only on the differences I made and not see the
entire document.
[Screencast from 2024-09-05
16-38-40.webm](https://github.com/user-attachments/assets/25d44a51-bcac-47d5-a2ec-cadae4d108d4)

A checkbox was added to the Version View allowing user to decide if
he/she wants to see only modified fields or the entire documents.
#7981 - mention this feature and also in discord

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

## Type of change

<!-- Please delete options that are not relevant. -->

- [v] New feature (non-breaking change which adds functionality)

## Checklist:

- [  ] Existing test suite passes locally with my changes
(Actually it's stuck on S3 upload test , note related to my code)

One lat question - should we really translate text for all locales ? or
we can leave it undefined for now ?(besides english)

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2025-01-28 21:36:07 +00:00
Patrik
989140b992 fix(ui): adds title attribute to Logout button for tooltip (#10851)
Native tooltip was missing from the `Logout` button because it was
missing the `title` attribute.

Adds `title` attribute to the `Logout` button to display native tooltip.

![Screenshot 2025-01-28 at 2 11
07 PM](https://github.com/user-attachments/assets/01f42877-8e01-4cd2-a064-e6c6eb77f216)

Fixes #10773
2025-01-28 14:36:09 -05:00
Said Akhrarov
8a6d995425 fix(ui): correctly reset blocksDrawer search state after close (#10847)
### What?
This PR fixes an issue where after closing the `BlocksDrawer` component
after performing a search, stale `Blocks` were shown the next time it
was open.

### Why?
To properly show all blocks when the `BlocksDrawer` is open after being
closed with a filtered search.

### How?
The `BlocksDrawer` was simply checking the existence of the
`isModalOpen` function instead of calling it as expected.

Fixes #10843

Before:

https://github.com/user-attachments/assets/5f41012d-ca84-41b4-9861-d5e0cb2579f6


After:

[Editing---Block-Field-after--Payload.webm](https://github.com/user-attachments/assets/4bd1ab11-f9a0-438f-a2e6-2ff0aba3e53d)

---------

Co-authored-by: Patrik Kozak <patrik@payloadcms.com>
2025-01-28 13:21:42 -05:00
Patrik
e65a04a20e templates: adds landing page to blank template (#10769)
This addition enhances the `Blank` template by adding a simple front-end
to ensure a better out-of-the-box experience.

When deploying the template to platforms like `Payload Cloud`, `Vercel`,
or similar services, users would previously encounter a `404` or
`not-found` page on the front-end `/` route unless explicitly handled.

With this update, the template now includes a minimal front-end that
renders a basic page at route `/`.

### Notes

- The added front-end is entirely optional.

- If users prefer to use the `Blank` template as a starting point for a
back-end-only solution or plan to integrate with a different front-end
framework, they can simply delete the `(frontend)` folder and proceed as
before.

`Logged out`:

![Screenshot 2025-01-28 at 10 26
01 AM](https://github.com/user-attachments/assets/f6cd99bd-9746-4d0e-910f-2322a671c6b3)

`Logged in`:

![Screenshot 2025-01-28 at 10 25
42 AM](https://github.com/user-attachments/assets/27c0bbfb-bd94-4e3c-9bb9-332aa3ccc8cc)

`Mobile`:

![Screenshot 2025-01-28 at 10 25
14 AM](https://github.com/user-attachments/assets/370869b4-c5e5-4b17-bff6-3514e7baffc7)
2025-01-28 11:39:29 -05:00
Jacob Fletcher
57f72185f8 chore(deps): upgrades react-diff-viewer-continued to v4.0.4 to suppress react 19 warnings and use ESM imports (#10834)
The `react-diff-viewer-continued` package now includes React 19 in its
peer dependencies thanks to
https://github.com/Aeolun/react-diff-viewer-continued/pull/56. This new
version also exports as ESM by default ftw.
2025-01-28 11:31:33 -05:00
Jessica Chowdhury
9c31a52329 fix: checks for localization to prevent publish button breaking (#10844)
This [PR](https://github.com/payloadcms/payload/pull/9438) introduced a
bug with the publish button, an error was being thrown when localization
is false. Updated the logic breaking the publish button to safely check
whether localization exists.
2025-01-28 14:39:29 +00:00
Jessica Chowdhury
9b497414fe feat: allow publish and publish specific locale buttons to be swapped (#9438)
### What?
Adds new feature to change the default behavior of the Publish button.

When localization is enabled, you can now choose whether to publish all
locales (default) or publish the active locale only.

<img width="401" alt="Screenshot 2024-11-22 at 12 03 20 PM"
src="https://github.com/user-attachments/assets/757383b9-3bf9-4816-8223-a907b120912e">

### Why?

Since implementing the ability to publish a specific locale, users have
reported that having this option as the default button would be
preferred in some cases.

### How?

Add `defaultLocalePublishOption` to your localization config and set it
to 'active':

```ts
  localization: {
    defaultLocalePublishOption: 'active',
    // the rest of your localization config
  },
```

---------

Co-authored-by: Anders Semb Hermansen <anders.hermansen@gmail.com>
2025-01-28 11:24:07 +00:00
Said Akhrarov
8952662db9 docs: fix links and formatting (#10835)
### What?
This PR fixes many links in the docs as well as a few formatting and
grammar issues.

### Why?
To properly link users to the correct destination in the docs and
present well-formatted docs.

### How?
Changes to a few files in `docs/`
2025-01-27 22:50:54 -07:00
Jarrod Flesch
a835518232 fix(ui): adds prev value on form state validat functions (#10832)
### What?
When hitting the form-state endpoint, previousValue was undefined.

### Why?
It was not being passed through.

### How?
Sets previousValue as the initial value of the field.

Fixes https://github.com/payloadcms/payload/issues/10826
2025-01-27 22:08:34 +00:00
Jacob Fletcher
82f1bb9864 perf: skips field validations until the form is submitted (#10580)
Field validations can be expensive, especially custom validations that
are async or highly complex. This can lead to slow form state response
times when generating form state for many such fields. Ideally, we only
run validations on fields whose values have changed. This is not
possible, however, because field validation functions might reference
_other_ field values with their args, and there is no good way of
detecting exactly which fields should run in this case. The next best
thing here is to only run validations _after the form has been
submitted_, and then every `onChange` event thereafter until a
successful submit has taken place. This is an elegant solution because
we currently don't _render_ field errors until submission anyway.

This change will significantly speed up form state response times, at
least until the form has been submitted. From then on, all field
validations will run regardless, just as they do now. If custom
validations continue to slow down form state response times, there is a
new `event` arg introduced in #10738 that can be used to control whether
heavy operations occur on change or on submit.

Related: #10638
2025-01-27 20:21:33 +00:00
Jacob Fletcher
0acaf8a7f7 fix: field paths within hooks (#10638)
Field paths within hooks are not correct.

For example, an unnamed tab containing a group field and nested text
field should have the path:
- `myGroupField.myTextField`

However, within hooks that path is formatted as:
- `_index-1.myGroupField.myTextField`

The leading index shown above should not exist, as this field is
considered top-level since it is located within an unnamed tab.

This discrepancy is only evident through the APIs themselves, such as
when creating a request with invalid data and reading the validation
errors in the response. Form state contains proper field paths, which is
ultimately why this issue was never caught. This is because within the
admin panel we merge the API response with the current form state,
obscuring the underlying issue. This becomes especially obvious in
#10580, where we no longer initialize validation errors within form
state until the form has been submitted, and instead rely solely on the
API response for the initial error state.

Here's comprehensive example of how field paths _should_ be formatted:

```
{
  // ...
  fields: [
    {
      // path: 'topLevelNamedField'
      // schemaPath: 'topLevelNamedField'
      // indexPath: ''
      name: 'topLevelNamedField',
      type: 'text',
    },
    {
      // path: 'array'
      // schemaPath: 'array'
      // indexPath: ''
      name: 'array',
      type: 'array',
      fields: [
        {
          // path: 'array.[n].fieldWithinArray'
          // schemaPath: 'array.fieldWithinArray'
          // indexPath: ''
          name: 'fieldWithinArray',
          type: 'text',
        },
        {
          // path: 'array.[n].nestedArray'
          // schemaPath: 'array.nestedArray'
          // indexPath: ''
          name: 'nestedArray',
          type: 'array',
          fields: [
            {
              // path: 'array.[n].nestedArray.[n].fieldWithinNestedArray'
              // schemaPath: 'array.nestedArray.fieldWithinNestedArray'
              // indexPath: ''
              name: 'fieldWithinNestedArray',
              type: 'text',
            },
          ],
        },
        {
          // path: 'array.[n]._index-2'
          // schemaPath: 'array._index-2'
          // indexPath: '2'
          type: 'row',
          fields: [
            {
              // path: 'array.[n].fieldWithinRowWithinArray'
              // schemaPath: 'array._index-2.fieldWithinRowWithinArray'
              // indexPath: ''
              name: 'fieldWithinRowWithinArray',
              type: 'text',
            },
          ],
        },
      ],
    },
    {
      // path: '_index-2'
      // schemaPath: '_index-2'
      // indexPath: '2'
      type: 'row',
      fields: [
        {
          // path: 'fieldWithinRow'
          // schemaPath: '_index-2.fieldWithinRow'
          // indexPath: ''
          name: 'fieldWithinRow',
          type: 'text',
        },
      ],
    },
    {
      // path: '_index-3'
      // schemaPath: '_index-3'
      // indexPath: '3'
      type: 'tabs',
      tabs: [
        {
          // path: '_index-3-0'
          // schemaPath: '_index-3-0'
          // indexPath: '3-0'
          label: 'Unnamed Tab',
          fields: [
            {
              // path: 'fieldWithinUnnamedTab'
              // schemaPath: '_index-3-0.fieldWithinUnnamedTab'
              // indexPath: ''
              name: 'fieldWithinUnnamedTab',
              type: 'text',
            },
            {
              // path: '_index-3-0-1'
              // schemaPath: '_index-3-0-1'
              // indexPath: '3-0-1'
              type: 'tabs',
              tabs: [
                {
                  // path: '_index-3-0-1-0'
                  // schemaPath: '_index-3-0-1-0'
                  // indexPath: '3-0-1-0'
                  label: 'Nested Unnamed Tab',
                  fields: [
                    {
                      // path: 'fieldWithinNestedUnnamedTab'
                      // schemaPath: '_index-3-0-1-0.fieldWithinNestedUnnamedTab'
                      // indexPath: ''
                      name: 'fieldWithinNestedUnnamedTab',
                      type: 'text',
                    },
                  ],
                },
              ],
            },
          ],
        },
        {
          // path: 'namedTab'
          // schemaPath: '_index-3.namedTab'
          // indexPath: ''
          label: 'Named Tab',
          name: 'namedTab',
          fields: [
            {
              // path: 'namedTab.fieldWithinNamedTab'
              // schemaPath: '_index-3.namedTab.fieldWithinNamedTab'
              // indexPath: ''
              name: 'fieldWithinNamedTab',
              type: 'text',
            },
          ],
        },
      ],
    },
  ]
}
```
2025-01-27 14:41:35 -05:00
Jarrod Flesch
9f9919d2c6 fix(next): remove toString coercion inside getDocumentPermissions (#10828)
### What?
When the doc permissions were retrieved from the DB, we were coercing
them into strings even when they should not have been.

### Why?
Usage of `id.toString()`

### How?
Remove `id.toString()`. The id will be correct by this point and we
should never coerce id's like this.

Fixes https://github.com/payloadcms/payload/issues/8218
2025-01-27 14:16:33 -05:00
Tib
95e81d8d96 docs: fix typo (#10824)
Fixing small typo
2025-01-27 19:08:37 +00:00
Jessica Chowdhury
ffe8020916 fix(translations): adds et to import file (#10823)
### What?
Error thrown when initiating the Estonian language (`et`) from
`@payloadcms/translations`
<img width="896" alt="Screenshot 2025-01-27 at 3 17 49 PM"
src="https://github.com/user-attachments/assets/27603c75-d713-4f11-b141-dc293d51800c"
/>

### How?
`et` needed to be added to `importDateFNSLocale`. Tested after this
change and the error is no longer present.

Fixes #10817
2025-01-27 15:40:24 +00:00
felismargarita
0d81ff2f59 fix(ui): hide the restore button's empty submenu in a draft version (#10756)
### What?
When the draft functionality is enabled, in the version history page,
_Restore this version_ renders an empty submenu popup.
### Why?
Restore this version button has below 2 basic scenarios:
1. restore a previously published version. 
In this scenario, this button has a second option _Restore as a draft_
which is in the button submenu area, it works perfectly.
2. restore a draft version
As a draft version, there should not have a second option _Restore as a
draft_, because it is a draft version itself, so this second option and
the submenu is not required in this scenario, but actually it shows an
empty submenu which is not a good idea
### How?
Check if this version is a draft version, if yes, no need to set a
render prop to the button component, so the empty popup won't be
displayed.

Fixes #10754

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-01-27 10:31:36 -05:00
Germán Jabloñski
d6658b55a1 chore(templates): fix: the contact page of the website template throws an error in live preview (#10785)
Fixes: https://github.com/payloadcms/payload/issues/10787

The underlying problem is that there are types in the form builder
plugin that are unnecessarily `any` or `unknown`.
Here in the website template this was being circumvented with a function
that was not really needed (buildInitialFormState), and with new unknown
types (Value, Property and Data).

Since create-payload-app fetches from the latest commit instead of the
latest release, it is necessary to first merge
https://github.com/payloadcms/payload/pull/10789, and once the next
release is done this PR can be merged.
2025-01-27 15:26:36 +00:00
Said Akhrarov
8289588994 fix(ui): include check for parent permissions in renderField (#10729)
### What?
This PR fixes an issue for tab subfield permissions

### Why?
Permissions were not being correctly extracted when passing them down.

### How?
Properly extracts permissions when rendering fields inside the active tab.

Fixes #10720

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-01-27 09:45:10 -05:00
Jessica Chowdhury
7a398704a0 fix(plugin-nested-docs): update draft and published child docs on resave (#10454)
### What?
Fixes bug with **plugin-nested-docs**. The plugin should update the
breadcrumb data of any child documents when the parent doc is updated,
currently only the **draft** child document is updated.

### How?
Updates the resave function to fetch draft and published child docs.

Closes issue #10066
2025-01-27 13:28:15 +00:00
Wallerand Delevacq
c1c64a07a2 fix(plugin-multi-tenant): issue #10740 - "The following field is invalid: Assigned Tenant" (#10764)
### What?
Tenant ID is a `number` but the `beforeChange` hook shows it as a `string`.
Getting tenant ID from cookie does not work properly in PG

### Why?
A `ValidationError` is throwing when reading a pg numeric id from the cookie.

### How?
Adjust the id based on the idType on the collection.

Fixes #10740

---------

Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2025-01-24 22:29:49 -05:00
Germán Jabloñski
6a39279697 docs: improvements in rich text section (#9954)
- fixed broken links
- improved introduction of `overview` and `slate` sections
2025-01-24 17:33:43 -03:00
James Mikrut
ace755742c chore: improves routeError log safety (#10793)
Improves the logging that `routeError` throws.

Logs were sometimes being swallowed if there was a problem with the
incoming request. Now they will surface.
2025-01-24 19:06:01 +00:00
Francisco Lourenço
828b3b71c0 feat: allows fields to be collapsed in the version view diff (#8054)
## Description

Allows some fields to be collapsed in the version diff view. The fields
that can be collapsed are the ones which can also be collapsed in the
edit view, or that have visual grouping:
- `collapsible` 
- `group`
- `array`  (and their rows)
- `blocks` (and their rows)
- `tabs`

It also 
- Fixes incorrect indentation of some fields
- Fixes the rendering of localized tabs in the diff view
- Fixes locale labels for the group field
- Adds a field change count to each collapsible diff (could imagine this
being used in other places)
- Brings the indentation gutter back to help visualize multiple nesting
levels


## Future improvements
- Persist collapsed state across page reloads (sessionStorage vs
preferences)

## Screenshots

### Without locales

![comparison](https://github.com/user-attachments/assets/754be708-be6d-43b4-bbe3-5d64ab6a0f76)


### With locales
![comparison with
locales](https://github.com/user-attachments/assets/02fb47fb-fa38-4195-8376-67bfda7f282d)

-------------- 

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


## Type of change

<!-- Please delete options that are not relevant. -->

- [x] New feature (non-breaking change which adds functionality)

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
- [ ] ~I have made corresponding changes to the documentation~
2025-01-24 13:32:55 -05:00
Germán Jabloñski
92e6beb050 fix(plugin-form-builder): type of MessageField to object (#10792)
Modifying https://github.com/payloadcms/payload/pull/10789 to make
MessageField editor agnostic (i.e. works with Lexical or Slate or any
other editor)
2025-01-24 18:05:52 +00:00
Elliot DeNolf
eca4f47d7a ci: remove docker login, not functional for external contributors 2025-01-24 12:51:21 -05:00
Alessio Gravili
72a5c02d95 chore: uncomment collectionSlugs array in fields test suite, for resetDB to work properly (#10778)
This array was commented out as a remnant of the large on-demand RSC PR,
in which we commented out substantial portions of code
2025-01-24 10:01:20 -07:00
Germán Jabloñski
5603c1ce8d fix(plugin-form-builder): type of MessageField to SerializedEditorState (#10789)
Fixes https://github.com/payloadcms/payload/issues/10787

This is the first part of the fix for
https://github.com/payloadcms/payload/issues/10787.
The second part is https://github.com/payloadcms/payload/pull/10785

The `packages/plugin-form-builder/src/types.ts` file contains many more
types besides MessageField.message that are unnecessarily `unknown` or
`any`.

In this PR I'm only fixing that one because:
1. It's what's needed to fix the issue.
2. I want to avoid a breaking change (they should be improved in v4
though).

I don't think MessageField.message will cause a breaking change for
anyone, since it's based on a rich text field.
2025-01-24 15:22:08 +00:00
Jarrod Flesch
22633a6de6 fix(plugin-multi-tenant): remove tenant cookie on logout (#10761)
### What?
- Removes the tenant cookie when the user logs out
- Prevents double redirect to globals when no tenant is selected

### Why?
There were a couple scenarios where the cookie and the tenant did not
match, ie if you logged into 1 tenant, and then out and then into
another tenant.
2025-01-24 10:10:49 -05:00
Elliot DeNolf
d6ae07dec6 ci: update CODEOWNERS 2025-01-24 08:59:59 -05:00
Alessio Gravili
344b23139e ci: fix run e2e command (#10779) 2025-01-24 03:47:49 +00:00
Alessio Gravili
b9d3250117 chore: migrate outdated @payloadcms/next/utilities imports (#10777) 2025-01-24 01:58:45 +00:00
Alessio Gravili
03f7bdf1ee chore: disable bun run test buttons if bun extension is installed (#10775)
If the bun extension is installed, a "Run Test" button is displayed in
int test files. Clicking it will use bun to run those tests, which will
always fail.

This PR disables that test button, as it's useless in our repo

![CleanShot 2025-01-23 at 17 39
03@2x](https://github.com/user-attachments/assets/918fa729-8076-4214-a3d2-f824a4fbfc34)

The real button here is "Run". Clicking "Run Test" instead will use bun
and fail. Confusing, right?
2025-01-24 00:41:06 +00:00
Alessio Gravili
59545b5fe5 templates: ensure lexical link validation does not break for internal links (#10771) 2025-01-23 22:42:10 +00:00
Germán Jabloñski
b76401c101 chore(templates): fix eslint errors in plugin template (#10770)
Additionally, the scope of `pnpm eslint` has been expanded to cover the
entire project, not only src.
2025-01-23 21:01:54 +00:00
Jacob Fletcher
a05240a853 perf: only validate filter options on submit (#10738)
Field validations currently run very often, such as within form state on
type. This can lead to serious performance implications within the admin
panel if those validation functions are async, especially if they
perform expensive database queries. One glaring example of this is how
all relationship and upload fields perform a database lookup in order to
evaluate that the given value(s) satisfy the defined filter options. If
the field is polymorphic, this can happen multiple times over, one for
each collection. Similarly, custom validation functions might also
perform expensive tasks, something that Payload has no control over.

The fix here is two-fold. First, we now provide a new `event` arg to all
`validate` functions that allow you to opt-in to performing expensive
operations _only when documents are submitted_, and fallback to
significantly more performant validations as form state is generated.
This new pattern will be the new default for relationship and upload
fields, however, any custom validation functions will need to be
implemented in this way in order to take advantage of it. Here's what
that might look like:

```
[
  // ...
  {
    name: 'text'
    type: 'text',
    validate: async (value, { event }) => {
      if (event === 'onChange') {
        // Do something highly performant here
        return true
      }
      
      // Do something more expensive here
      return true
    }
  }
]
```

The second part of this is to only run validations _after the form as
been submitted_, and then every change event thereafter. This work is
being done in #10580.
2025-01-23 15:10:31 -05:00
Alessio Gravili
9f2bca104b fix(richtext-lexical): afterRead hooks were not awaited (#10747)
This was a tricky one.

Fixes https://github.com/payloadcms/payload/issues/10700

May potentially fix 9163

This could have also been causing glitchyness related to things like
lexical upload / relationship / link node population.

## Issue and solution explained

The lexical field afterRead hook is responsible for executing afterRead
hooks (this includes relationship population) for all sub-nodes, e.g.
upload or block nodes.

Any field and population promises that were created in the process of
traversing the lexical editor state were added to the parent
`fieldPromises` and `populationPromises` array.

Now this lexical `afterRead` hook, which is responsible for creating and
adding all field and population promises of its sub-nodes to the parent
`fieldPromises` and `populationPromises` arrays, was itself part of the
**same** `fieldPromises` array.

The execution of this lexical `afterRead` hook itself is happening while
the `fieldPromises` array is being awaited. This means that new field
and population promises were being added to this same array DURING the
process of awaiting all promises of this array.

As a result, any promises dynamically added while awaiting the initial
set of fieldPromises were not included in the initial `Promise.all()`
awaiting process, leading to unhandled promises.

This PR resolves the issue by ensuring that promises are repeatedly
awaited until no new promises remain in the arrays. By continuously
awaiting the `fieldPromises` and `populationPromises` until all
dynamically added promises are fully resolved, this PR ensures that any
promises added during the processing of a parent promise are also
properly awaited. This guarantees that no promises are skipped,
preserving the intended behavior of the afterRead hook
2025-01-23 14:44:21 -05:00
Said Akhrarov
ec1a441ed7 docs: adds limit, page, sort, where, and joins to list of rest query params (#10751)
<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

### Why?

### How?

Fixes #

-->
### What?
This PR adds a few missing query params to the list of REST query params
available on the rest-api overview page of the docs.

### Why?
To better inform users of more utilities available to them right in the
overview page.

### How?
Changes to `rest-api/overview.mdx`
2025-01-23 14:23:02 -05:00
Elliot DeNolf
b2ebf85082 chore(release): v3.19.0 [skip ci] 2025-01-23 13:38:39 -05:00
Sasha
c0ae994da2 fix: next.js rewrites were not respected for rest api (#10759)
Fixes https://github.com/payloadcms/payload/issues/10655
2025-01-23 13:36:33 -05:00
Germán Jabloñski
5689c6527b chore(templates): fix eslint errors in website template (#10752) 2025-01-23 15:30:20 -03:00
Sasha
2d8ff7218a fix(db-mongodb): v2-v3 migration versions docs of collections and globals without relationship fields (#10755)
Fixes https://github.com/payloadcms/payload/issues/10753
2025-01-23 12:33:43 -05:00
Paul
61a2a9c3a3 templates: update website readmes for additional information on jobs and revalidation (#10758) 2025-01-23 17:33:07 +00:00
Sasha
d601300034 fix(db-mongodb): querying polymorphic relationships with the all operator (#10704)
Fixes https://github.com/payloadcms/payload/issues/10678
2025-01-23 18:23:50 +02:00
Jacob Fletcher
e5b3da972f docs: moves collection and globals admin docs to their respective config overviews (#10743)
Similar to #10742. Collection and global-level admin options are
currently documented within the "admin > collections" and "admin >
globals" pages, respectively. This makes them hard to find because
users, myself included, intuitively navigate to the collection and
global overview docs to locate this information before realizing it
lives elsewhere. Now, they are rendered within "configuration >
collections" and "configuration > globals" as expected and the old pages
have been removed altogether.
2025-01-23 01:54:31 -05:00
Jacob Fletcher
0ca37364ea docs: moves customizing fields doc to fields overview (#10742)
Continuation of #10741. Field-level admin options, including the
conditional logic and custom field components, are currently documented
within the "admin > customizing views" page. This makes them hard to
find because users, myself included, intuitively navigate to the fields
overview doc first to locate this information. Now, they are rendered
within "fields > overview" as expected. This should help keep the user
from jumping around from doc to doc and getting lost.
2025-01-22 23:58:05 -05:00
Jacob Fletcher
8b3e2ff5e5 docs: adds examples of typed custom field components (#10741)
Although the "customizing fields" doc provides a big picture overview of
how to create custom field components, it is not explicit enough for
developers to know exactly where to start. For example, it can be
challenging to import the correct types when building these components,
and the natural place to go looking for this information is on the
fields docs themselves. Now, each field doc has its own dedicated
"custom components" section which provides concrete examples for fields
and field labels in both server and client component format, with more
examples to come over time such as using inputs directly, etc. In the
same vein, the "customizing fields" doc itself should probably be moved
to the fields overview section so it remains as intuitive as possible
when searching for this information.
2025-01-22 23:07:09 -05:00
Dan Ribbens
3501d47e2d fix(plugin-nested-docs): cannot update more than 10 child docs (#10737)
`limit` was defaulting to 10 in the call to find children that need to
be updated, now limit is 0 to get all children docs.
2025-01-22 17:42:12 -05:00
Germán Jabloñski
4aaef5e63e feat(richtext-lexical): make decoratorNodes and blocks selectable. Centralize selection and deletion logic (#10735)
- Blocks can now be selected (only inline blocks were possible before).
- Any DecoratorNode that users create will have the necessary logic out
of the box so that they are selected with a click and deleted with
backspace/delete.
- By having the code for selecting and deleting centralized, a lot of
repetitive code was eliminated
- More performant code due to the use of event delegation. There is only
one listener, previously there was one for each decoratorNode.
- Heuristics to exclude scenarios where you don't want to select the
node: if it is inside the DecoratorNode, but is also inside a button,
input, textarea, contentEditable, .react-select, .code-editor or
.no-select-decorator. That last one was added as a means of opt-out.
- Fix #10634

Note: arrow navigation will be introduced in a later PR.



https://github.com/user-attachments/assets/92f91cad-4f70-4f72-a36f-c68afbe33c0d
2025-01-22 22:28:48 +00:00
Patrik
f181f97d4e fix(ui): filters out upload specific fields for bulk editing (#10726)
### What

This PR adds a filtering mechanism to exclude certain reserved fields
from being displayed in the `Edit Many` drawer for bulk uploads. This
ensures that only relevant fields are available for bulk editing.

### Why

Fields like `filename`, `mimeType`, and `filesize` are not intended to
be edited in bulk. Filtering these fields streamlines the interface and
focuses on fields that are meaningful for bulk operations.

### How

- Introduced a `filterOutUploadFields` utility to exclude reserved
fields from the field selection in bulk uploads.
- Applied this filter to the `Edit Many` drawer, ensuring a more
relevant and user-friendly experience.
- Reserved fields include properties like `file`, `mimeType`, `url`,
`width`, `height`, and others that are not applicable for bulk editing.
2025-01-22 16:44:17 -05:00
Alessio Gravili
0c5321e6f8 chore: temporarily revert 10597 (#10718)
Temporarily reverts 10597 to fix plugin-oauth
2025-01-22 16:36:54 -05:00
Jarrod Flesch
9a8769967c chore(plugin-multi-tenant): test suite enhancements (#10732)
### What?
Updates test suite multi-tenant config as a better example.

### Why?
So it is easier to follow and write logical tests for in the future.
2025-01-22 16:28:18 -05:00
Patrik
be2c482054 feat(ui): adds edit many option for bulk uploads (#10646)
### What?

This PR introduces the ability to bulk edit multiple uploads
simultaneously within the `Edit all` option for bulk uploads. Users can
now select fields to update across all selected uploads in a single
operation.

### Why?

Managing multiple uploads individually can be time-consuming and
inefficient, especially when updating common fields. This feature
streamlines the process, improving user experience and productivity when
handling bulk uploads.

### How?

* Added an `Edit Many` drawer component specific to bulk uploads that
allows users to select fields for bulk editing.
* Enhanced the FormsManager and related logic to ensure updates are
applied consistently across all selected uploads.

![Screenshot 2025-01-21 at 3 16
49 PM](https://github.com/user-attachments/assets/ef1f4a12-95a6-4b21-8efa-5280df0917fc)
2025-01-22 16:20:13 -05:00
Paul
67f7c9513f fix(ui): admin description not being respected on tabs and padding issues with tab descriptions (#10710)
- Fixes collection and tab descriptions not having a bottom padding:
- Deprecates `description` on tabs config in favour of
`admin.description` but supports both
- Adds test to make sure `admin.description` renders with static string
and function too for tabs
2025-01-22 17:17:17 +00:00
Elliot DeNolf
7010d93e94 ci: dockerhub login to increase rate-limit (#10727)
Docker Hub has rate-limits for anonymous users. This adds logging in,
which will increase the limit.

Example error message
```
Unable to find image 'mongo:6' locally
  docker: Error response from daemon: toomanyrequests: 
You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limit.
  ```
  
 Example error in pipeline: https://github.com/payloadcms/payload/actions/runs/12911899766/job/36005722765?pr=10723
2025-01-22 17:12:04 +00:00
Paul
9bb27afaf5 fix(richtext-lexical): improved regex matchers for absolute and relative URLs to make autolinking more reliable (#10725)
Fixes an issue where pasting text over a selection will automatically
add it as a link instead of replacing the text. This is caused by poor
regex matching in the case of relative URLs.

Added tests and strengthened both absolute and relative URL matchers
2025-01-22 16:53:56 +00:00
Jarrod Flesch
e6d02600e1 fix(plugin-multi-tenant): selected tenant could become incorrect when navigating out of doc (#10723)
### What?
When switching tenants from within a document and then navigating back
out to the list view, the tenant would not be set correctly.

### Why?
This was because we handle the tenant selector selection differently
when viewing a document.

### How?
Now when you navigate out, the page will refresh the cookie.

Also adds test suite config that shows how the dom can be used to
manipulate styles per tenant.
2025-01-22 11:37:18 -05:00
Alessio Gravili
c1b912d5e5 fix: browser validation error when visiting account page (#10717)
Fixes error when visiting account page:

![CleanShot 2025-01-21 at 23 58
48@2x](https://github.com/user-attachments/assets/01502702-319f-46fd-9197-b345eab7fc86)
2025-01-22 07:19:06 +00:00
Said Akhrarov
d8682f2147 docs: adds info on useSelection and useStepNav hooks (#10683)
### What?
This PR adds information and examples on the `useSelection` and
`useStepNav` hooks.

### Why?
To provide more information on the React hooks available to end-users.

### How?
Changes to `admin/hooks.mdx`
2025-01-22 00:11:47 -07:00
Boyan Bratvanov
6d43910018 docs: multi-tenant plugin - remove @beta and fix npm url (#10697)
Running `pnpm add @payloadcms/plugin-multi-tenant@beta` will install
`v.0.0.1` which is outdated:

https://www.npmjs.com/package/@payloadcms/plugin-multi-tenant?activeTab=versions

I suspect the intention was to remove `@beta` from the plugin install
instructions with yesterday's Payload `v3.18.0` release which also
bumped `plugin-multi-tenant` to `v3.18.0`, but this might have been
missed.
2025-01-22 00:08:38 -07:00
Paul
5e4a1d48ae templates: fix potential error in the initial form state caused by type mismatch (#10713) 2025-01-21 15:34:21 -06:00
David Murdoch
b55342d9af chore(examples): change to useClickableCard to use AbortController (#10680)
* introduce AbortController to the event listeners in useClickableCard
* in an attempt to avoid ugly boolean check
* suggested pattern from
https://kettanaito.com/blog/dont-sleep-on-abort-controller

<!--

Thank you for the PR! Please go through the checklist below and make
sure you've completed all the steps.

Please review the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository if you haven't already.

The following items will ensure that your PR is handled as smoothly as
possible:

- PR Title must follow conventional commits format. For example, `feat:
my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not
immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic
behind a change

### What?

Add AbortController to event listeners in useClickableCard.

### Why?

Following Theo's [video](https://www.youtube.com/watch?v=2sdXSczmvNc)
about the [blog
post](https://kettanaito.com/blog/dont-sleep-on-abort-controller) about
AbortController I came across a bit of code in examples that looked a
bit ugly and thought the AbortController could simplify it a bit
(especially the checks for the DOM node in the removal part).

### How?

Just re-writing the code in a different way. Though, I admit I'm not
wholly sure where the Cards are being used and for what purpose so I
haven't checked that there is no difference to the behaviour of the
code.

Fixes #
Not so much a fix but a different way to write the removal of event
listeners.
-->
2025-01-21 15:34:11 -06:00
Jarrod Flesch
2a98c8445e fix(plugin-multi-tenant): corrects user type in userHasAccessToAllTenants fn (#10707)
### What?
User type was inflexible.

### How?
Internally use as unknown.
2025-01-21 12:44:37 -05:00
Jarrod Flesch
a9c08323cc fix(plugin-multi-tenant): prevent throwing when no user exists (#10699)
### What?
Fixes issue where the provider would throw an error and prevent the
login screen from loading if there was no user.

### Why?
Missing try/catch around tenant find for the provider. (Missed because
test suites have autoLogin: true)

### How?
Adds try/catch around find query.
2025-01-21 12:07:47 -05:00
Jarrod Flesch
3e0baf58c0 chore: adds plugin-multi-tenant scope for pr title (#10706)
### What?
Adds `plugin-multi-tenant` scope for PR titles

### Why?
Cannot scope multi-tenant PR's properly
2025-01-21 12:07:10 -05:00
5746 changed files with 1258 additions and 705552 deletions

View File

@@ -1,29 +0,0 @@
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{js,ts,tsx}]
indent_style = space
indent_size = 2
[*.html]
indent_style = space
indent_size = 2
[*.scss]
indent_style = space
indent_size = 2
[*.mdx]
indent_style = space
indent_size = 2
[*.json]
indent_style = space
indent_size = 2
[*.md]
indent_style = space
indent_size = 2

View File

@@ -1,33 +0,0 @@
# lint and format
ae7d6f97d205491390f15850e5104c7abded1550
1fbda85cd04a774cb978778b0f813001664c53dd
# prettier all templates
75a428ddc4672903455998eaba7ae9f9d710bf85
# re-run prettier and eslint everywhere again
cdaa0acd61d3001407609915bd573b78565d5571
# prettier write again
dfac7395fed95fc5d8ebca21b786ce70821942bb
# lint and format plugin-cloud
fb7d1be2f3325d076b7c967b1730afcef37922c2
# lint and format create-payload-app
5fd3d430001efe86515262ded5e26f00c1451181
# 3.0 prettier & lint everywhere
6789e61488a1d3de56f472ac3214faf344030005
# 3.0 prettier & lint everywhere again
83fd4c66222d7846eeb5cc332dfa99bf1e830831
# Upgrade to typescript-eslint v8, then prettier & lint everywhere
86fdad0bb8ab27810599c8a32f3d8cba1341e1df
# Prettier and lint remaining db packages
7fd736ea5b2e9fc4ef936e9dc9e5e3d722f6d8bf
# Bump all eslint deps, lint and format
03291472d6e427ff94e61fca0616cca7796a3a95

2
.gitattributes vendored
View File

@@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto eol=lf

29
.github/CODEOWNERS vendored
View File

@@ -1,29 +0,0 @@
# Order matters. The last matching pattern takes precedence.
# Approvals are not required currently but may be enabled in the future.
### Package Exports ###
/**/exports/ @denolfe @jmikrut @DanRibbens
### Packages ###
/packages/plugin-cloud*/src/ @denolfe
/packages/email-*/src/ @denolfe
/packages/storage-*/src/ @denolfe
/packages/create-payload-app/src/ @denolfe
/packages/eslint-*/ @denolfe @AlessioGr
### Templates ###
/templates/_data/ @denolfe
/templates/_template/ @denolfe
### Build Files ###
/tsconfig.json @denolfe
/**/tsconfig*.json @denolfe
/jest.config.js @denolfe
/**/jest.config.js @denolfe
### Root ###
/package.json @denolfe
/scripts/ @denolfe
/.husky/ @denolfe
/.vscode/ @denolfe @AlessioGr
/.github/ @denolfe

View File

@@ -1,77 +0,0 @@
name: Functionality Bug
description: '[REPRODUCTION REQUIRED] - Create a bug report'
labels: ['status: needs-triage', 'validate-reproduction']
body:
- type: textarea
attributes:
label: Describe the Bug
validations:
required: true
- type: input
id: reproduction-link
attributes:
label: Link to the code that reproduces this issue
description: >-
_REQUIRED_: Please provide a link to your reproduction. Note, if the URL is invalid (404 or a private repository), we may close the issue.
Either use `pnpx create-payload-app@latest -t blank` then push to a repo or follow the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md) for more information.
validations:
required: true
- type: textarea
attributes:
label: Reproduction Steps
description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken.
validations:
required: true
- type: dropdown
attributes:
label: Which area(s) are affected? (Select all that apply)
multiple: true
options:
- 'Not sure'
- 'area: core'
- 'area: docs'
- 'area: templates'
- 'area: ui'
- 'db-mongodb'
- 'db-postgres'
- 'db-sqlite'
- 'db-vercel-postgres'
- 'email-nodemailer'
- 'plugin: cloud'
- 'plugin: cloud-storage'
- 'plugin: form-builder'
- 'plugin: nested-docs'
- 'plugin: richtext-lexical'
- 'plugin: richtext-slate'
- 'plugin: search'
- 'plugin: sentry'
- 'plugin: seo'
- 'plugin: stripe'
- 'plugin: other'
validations:
required: true
- type: textarea
attributes:
label: Environment Info
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions.
render: text
placeholder: |
Payload:
Node.js:
Next.js:
validations:
required: true
- type: markdown
attributes:
value: Before submitting the issue, go through the steps you've written down to make sure the steps provided are detailed and clear.
- type: markdown
attributes:
value: Contributors should be able to follow the steps provided in order to reproduce the bug.
- type: markdown
attributes:
value: These steps are used to add integration tests to ensure the same issue does not happen again. Thanks in advance!

View File

@@ -1,40 +0,0 @@
name: Design Issue
description: '[SCREENSHOT REQUIRED] - Create a design issue report'
labels: ['status: needs-triage', 'v3', 'area: ui']
body:
- type: textarea
attributes:
label: Describe the Bug.
description: >-
_REQUIRED:_ Please a screenshot/video of the issue along with a detailed description of the problem.
validations:
required: true
- type: textarea
attributes:
label: Reproduction Steps
description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken.
validations:
required: true
- type: textarea
attributes:
label: Environment Info
description: Paste output from `pnpm payload info` _or_ Payload, Node.js, and Next.js versions.
render: text
placeholder: |
Payload:
Node.js:
Next.js:
validations:
required: true
- type: markdown
attributes:
value: Before submitting the issue, go through the steps you've written down to make sure the steps provided are detailed and clear.
- type: markdown
attributes:
value: Contributors should be able to follow the steps provided in order to reproduce the bug.
- type: markdown
attributes:
value: These steps are used to add integration tests to ensure the same issue does not happen again. Thanks in advance!

View File

@@ -1,47 +0,0 @@
name: v2 Bug Report
description: Report a bug for Payload v2. ONLY CRITICAL bugs will be fixed in v2.
labels: ['status: needs-triage', 'v2']
body:
- type: markdown
attributes:
value: |
ONLY CRITICAL bugs will be fixed in v2.
- type: input
id: reproduction-link
attributes:
label: Link to reproduction
description: Want us to look into your issue faster? Follow the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md) for more information.
validations:
required: false
- type: textarea
attributes:
label: Describe the Bug
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
description: Steps to reproduce the behavior, please provide a clear description of how to reproduce the issue, based on the linked minimal reproduction. Screenshots can be provided in the issue body below. If using code blocks, make sure that [syntax highlighting is correct](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) and double check that the rendered preview is not broken.
validations:
required: true
- type: input
id: version
attributes:
label: Payload Version
description: What version of Payload are you running?
validations:
required: true
- type: input
id: adapters-plugins
attributes:
label: Adapters and Plugins
description: What adapters and plugins are you using? ie. db-mongodb, db-postgres, bundler-webpack, etc.
- type: markdown
attributes:
value: Before submitting the issue, go through the steps you've written down to make sure the steps provided are detailed and clear.
- type: markdown
attributes:
value: Contributors should be able to follow the steps provided in order to reproduce the bug.
- type: markdown
attributes:
value: These steps are used to add integration tests to ensure the same issue does not happen again. Thanks in advance!

View File

@@ -1,13 +0,0 @@
---
name: Documentation Issue
about: Suggest fix to Payload documentation
labels: 'documentation'
---
# Documentation Issue
<!--- Please provide a summary of the documentation issue -->
## Additional Details
<!--- Provide any other additional details -->

View File

@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Feature Request
url: https://github.com/payloadcms/payload/discussions
about: Suggest an idea to improve Payload in our GitHub Discussions
- name: Question about Payload
url: https://github.com/payloadcms/payload/discussions
about: Please ask Payload-related questions in our GitHub Discussions

View File

@@ -1,23 +0,0 @@
<!--
Thank you for the PR! Please go through the checklist below and make sure you've completed all the steps.
Please review the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository if you haven't already.
The following items will ensure that your PR is handled as smoothly as possible:
- PR Title must follow conventional commits format. For example, `feat: my new feature`, `fix(plugin-seo): my fix`.
- Minimal description explained as if explained to someone not immediately familiar with the code.
- Provide before/after screenshots or code diffs if applicable.
- Link any related issues/discussions from GitHub or Discord.
- Add review comments if necessary to explain to the reviewer the logic behind a change
### What?
### Why?
### How?
Fixes #
-->

View File

@@ -1,13 +0,0 @@
module.exports = {
env: {
es6: true,
node: true,
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/eslint-recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: ['@typescript-eslint'],
}

View File

@@ -1,74 +0,0 @@
# Release Commenter
This GitHub Action automatically comments on and/or labels Issues and PRs when a fix is released for them.
> [!IMPORTANT]
> 🔧 Heavily modified version of https://github.com/apexskier/github-release-commenter
## Fork Modifications
- Filters to closed PRs only
- Adds tag filter to support non-linear releases
- Better logging
- Moved to pnpm
- Uses @vercel/ncc for packaging
- Comments on locked issues by unlocking then re-locking
## How it works
Use this action in a workflow [triggered by a release](https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows#release). It will scan commits between that and the prior release, find associated Issues and PRs, and comment on them to let people know a release has been made. Associated Issues and PRs can be directly [linked](https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue) to the commit or manually linked from a PR associated with the commit.
## Inputs
**GITHUB_TOKEN**
A GitHub personal access token with repo scope, such as [`secrets.GITHUB_TOKEN`](https://docs.github.com/en/free-pro-team@latest/actions/reference/authentication-in-a-workflow#about-the-github_token-secret).
**comment-template** (optional)
Override the comment posted on Issues and PRs. Set to the empty string to disable commenting. Several variables strings will be automatically replaced:
- `{release_link}` - a markdown link to the release
- `{release_name}` - the release's name
- `{release_tag}` - the release's tag
**label-template** (optional)
Add the given label. Multiple labels can be separated by commas. Several variable strings will be automatically replaced:
- `{release_name}` - the release's name
- `{release_tag}` - the release's tag
**skip-label** (optional)
Skip processing if any of the given labels are present. Same processing rules as **label-template**. Default is "dependencies".
## Example
```yml
on:
release:
types: [published]
jobs:
release:
steps:
- uses: apexskier/github-release-commenter@v1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
comment-template: |
Release {release_link} addresses this.
```
## Known limitations
These are some known limitations of this action. I'd like to try to address them in the future.
- Non-linear releases aren't supported. For example, releasing a patch to a prior major release after a new major release has been bumped.
- Non-sequential releases aren't supported. For example, if you release multiple prereleases between two official releases, this will only create a comment for the first prerelease in which a fix is released, not the final release.
- The first release for a project will be ignored. This is intentional, as the use case is unlikely. Most projects will either have several alphas that don't need release comments, or won't use issues/PRs for the first commit.
- If a large number of things are commented on, you may see the error `Error: You have triggered an abuse detection mechanism. Please wait a few minutes before you try again.`. Consider using the `skip-label` input to reduce your load on the GitHub API.
## Versions
Workflows will automatically update the tags `v1` and `latest`, allowing you to reference one of those instead of locking to a specific release.

View File

@@ -1,32 +0,0 @@
name: Release Commenter
description: Comment on PRs and Issues when a fix is released
branding:
icon: message-square
color: blue
inputs:
GITHUB_TOKEN:
description: |
A GitHub personal access token with repo scope, such as
secrets.GITHUB_TOKEN.
required: true
comment-template:
description: |
Text template for the comment string.
required: false
default: |
Included in release {release_link}
label-template:
description: Add the given label. Multiple labels can be separated by commas.
required: false
skip-label:
description: Skip commenting if any of the given label are present. Multiple labels can be separated by commas.
required: false
default: 'dependencies'
tag-filter:
description: |
Filter tags by a regular expression. Must be escaped. e.g. 'v\\d' to isolate tags between major versions.
required: false
default: null
runs:
using: node20
main: dist/index.js

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
module.exports = {
testEnvironment: 'node',
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/dist/'],
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest'],
},
}

View File

@@ -1,33 +0,0 @@
{
"name": "release-commenter",
"version": "0.0.0",
"private": true,
"description": "GitHub Action to automatically comment on PRs and Issues when a fix is released.",
"license": "MIT",
"main": "dist/index.js",
"scripts": {
"build": "pnpm build:typecheck && pnpm build:ncc",
"build:ncc": "ncc build src/index.ts -t -o dist",
"build:typecheck": "tsc",
"clean": "rimraf dist",
"test": "jest"
},
"dependencies": {
"@actions/core": "^1.3.0",
"@actions/github": "^5.0.0"
},
"devDependencies": {
"@octokit/webhooks-types": "^7.5.1",
"@swc/jest": "^0.2.37",
"@types/jest": "^27.5.2",
"@types/node": "^20.16.5",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"@vercel/ncc": "0.38.1",
"concurrently": "^8.2.2",
"eslint": "^7.32.0",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"typescript": "^4.9.5"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,266 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`tests feature tests can apply labels 1`] = `
[
[
{
"issue_number": 123,
"labels": [
":dart: landed",
"release-current_tag_name",
"Release Name",
],
},
],
[
{
"issue_number": 7,
"labels": [
":dart: landed",
"release-current_tag_name",
"Release Name",
],
},
],
]
`;
exports[`tests main test 1`] = `
{
"graphql": [MockFunction] {
"calls": [
[
"
{
resource(url: "http://repository/commit/SHA1") {
... on Commit {
messageHeadlineHTML
messageBodyHTML
associatedPullRequests(first: 10) {
pageInfo {
hasNextPage
}
edges {
node {
bodyHTML
number
state
labels(first: 10) {
pageInfo {
hasNextPage
}
nodes {
name
}
}
timelineItems(itemTypes: [CONNECTED_EVENT, DISCONNECTED_EVENT], first: 100) {
pageInfo {
hasNextPage
}
nodes {
... on ConnectedEvent {
__typename
isCrossRepository
subject {
... on Issue {
number
}
}
}
... on DisconnectedEvent {
__typename
isCrossRepository
subject {
... on Issue {
number
}
}
}
}
}
}
}
}
}
}
}
",
],
[
"
{
resource(url: "http://repository/commit/SHA2") {
... on Commit {
messageHeadlineHTML
messageBodyHTML
associatedPullRequests(first: 10) {
pageInfo {
hasNextPage
}
edges {
node {
bodyHTML
number
state
labels(first: 10) {
pageInfo {
hasNextPage
}
nodes {
name
}
}
timelineItems(itemTypes: [CONNECTED_EVENT, DISCONNECTED_EVENT], first: 100) {
pageInfo {
hasNextPage
}
nodes {
... on ConnectedEvent {
__typename
isCrossRepository
subject {
... on Issue {
number
}
}
}
... on DisconnectedEvent {
__typename
isCrossRepository
subject {
... on Issue {
number
}
}
}
}
}
}
}
}
}
}
}
",
],
],
"results": [
{
"type": "return",
"value": Promise {},
},
{
"type": "return",
"value": Promise {},
},
],
},
"rest": {
"issues": {
"addLabels": [MockFunction],
"createComment": [MockFunction] {
"calls": [
[
{
"body": "Included in release [current_tag_name](http://current_release). Replacements: current_tag_name, current_tag_name.",
"issue_number": 3,
},
],
[
{
"body": "Included in release [current_tag_name](http://current_release). Replacements: current_tag_name, current_tag_name.",
"issue_number": 123,
},
],
[
{
"body": "Included in release [current_tag_name](http://current_release). Replacements: current_tag_name, current_tag_name.",
"issue_number": 7,
},
],
],
"results": [
{
"type": "return",
"value": Promise {},
},
{
"type": "return",
"value": Promise {},
},
{
"type": "return",
"value": Promise {},
},
],
},
"get": [MockFunction] {
"calls": [
[
{
"issue_number": 3,
},
],
[
{
"issue_number": 123,
},
],
[
{
"issue_number": 7,
},
],
],
"results": [
{
"type": "return",
"value": Promise {},
},
{
"type": "return",
"value": Promise {},
},
{
"type": "return",
"value": Promise {},
},
],
},
},
"repos": {
"compareCommits": [MockFunction] {
"calls": [
[
{
"base": "prior_tag_name",
"head": "current_tag_name",
},
],
],
"results": [
{
"type": "return",
"value": Promise {},
},
],
},
"listReleases": [MockFunction] {
"calls": [
[
{
"per_page": 100,
},
],
],
"results": [
{
"type": "return",
"value": Promise {},
},
],
},
},
},
}
`;

View File

@@ -1,399 +0,0 @@
import type * as githubModule from '@actions/github'
import type * as coreModule from '@actions/core'
import { mock } from 'node:test'
jest.mock('@actions/core')
jest.mock('@actions/github')
type Mocked<T> = {
-readonly [P in keyof T]: T[P] extends Function ? jest.Mock<T[P]> : jest.Mocked<Partial<T[P]>>
}
const github = require('@actions/github') as jest.Mocked<Mocked<typeof githubModule>>
const core = require('@actions/core') as jest.Mocked<Mocked<typeof coreModule>>
describe('tests', () => {
let mockOctokit: any = {}
let currentTag: string = 'current_tag_name'
;(core.warning as any) = jest.fn(console.warn.bind(console))
;(core.error as any) = jest.fn(console.error.bind(console))
let commentTempate: string = ''
let labelTemplate: string | null = null
const skipLabelTemplate: string | null = 'skip,test'
let tagFilter: string | RegExp | null = null
let simpleMockOctokit: any = {}
beforeEach(() => {
tagFilter = null
currentTag = 'current_tag_name'
;(github.context as any) = {
payload: {
repo: {
owner: 'owner',
repo: 'repo',
},
release: {
tag_name: currentTag,
},
repository: { html_url: 'http://repository' },
},
}
github.getOctokit.mockReset().mockImplementationOnce(((token: string) => {
expect(token).toBe('GITHUB_TOKEN_VALUE')
return mockOctokit
}) as any)
;(core.getInput as any).mockImplementation((key: string) => {
if (key == 'GITHUB_TOKEN') {
return 'GITHUB_TOKEN_VALUE'
}
if (key == 'comment-template') {
return commentTempate
}
if (key == 'label-template') {
return labelTemplate
}
if (key == 'skip-label') {
return skipLabelTemplate
}
if (key == 'tag-filter') {
return tagFilter
}
fail(`Unexpected input key ${key}`)
})
commentTempate =
'Included in release {release_link}. Replacements: {release_name}, {release_tag}.'
labelTemplate = null
simpleMockOctokit = {
rest: {
issues: {
get: jest.fn(() => Promise.resolve({ data: { locked: false } })),
createComment: jest.fn(() => Promise.resolve()),
addLabels: jest.fn(() => Promise.resolve()),
},
repos: {
listReleases: jest.fn(() =>
Promise.resolve({
data: [
{
name: 'Release Name',
tag_name: 'current_tag_name',
html_url: 'http://current_release',
},
{
tag_name: 'prior_tag_name',
html_url: 'http://prior_release',
},
],
}),
),
compareCommits: jest.fn(() =>
Promise.resolve({
data: { commits: [{ sha: 'SHA1' }] },
}),
),
},
},
graphql: jest.fn(() =>
Promise.resolve({
resource: {
messageHeadlineHTML: '',
messageBodyHTML:
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
associatedPullRequests: {
pageInfo: { hasNextPage: false },
edges: [],
},
},
}),
),
}
})
afterEach(() => {
expect(core.error).not.toHaveBeenCalled()
expect(core.warning).not.toHaveBeenCalled()
expect(core.setFailed).not.toHaveBeenCalled()
})
test('main test', async () => {
mockOctokit = {
...simpleMockOctokit,
rest: {
issues: {
get: jest.fn(() => Promise.resolve({ data: { locked: false } })),
createComment: jest.fn(() => Promise.resolve()),
addLabels: jest.fn(() => Promise.resolve()),
},
repos: {
listReleases: jest.fn(() =>
Promise.resolve({
data: [
{
tag_name: 'current_tag_name',
html_url: 'http://current_release',
},
{
tag_name: 'prior_tag_name',
html_url: 'http://prior_release',
},
],
}),
),
compareCommits: jest.fn(() =>
Promise.resolve({
data: { commits: [{ sha: 'SHA1' }, { sha: 'SHA2' }] },
}),
),
},
},
graphql: jest.fn(() =>
Promise.resolve({
resource: {
messageHeadlineHTML:
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #3.">Closes</span> <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="718013420" data-permission-text="Title is private" data-url="https://github.com/apexskier/github-release-commenter/issues/1" data-hovercard-type="issue" data-hovercard-url="/apexskier/github-release-commenter/issues/1/hovercard" href="https://github.com/apexskier/github-release-commenter/issues/1">#1</a>',
messageBodyHTML:
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
associatedPullRequests: {
pageInfo: { hasNextPage: false },
edges: [
{
node: {
bodyHTML:
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #4.">Closes</span> <span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #5.">Closes</span>',
number: 9,
labels: {
pageInfo: { hasNextPage: false },
nodes: [{ name: 'label1' }, { name: 'label2' }],
},
timelineItems: {
pageInfo: { hasNextPage: false },
nodes: [
{
isCrossRepository: true,
__typename: 'ConnectedEvent',
subject: { number: 1 },
},
{
isCrossRepository: false,
__typename: 'ConnectedEvent',
subject: { number: 2 },
},
{
isCrossRepository: false,
__typename: 'DisconnectedEvent',
subject: { number: 2 },
},
{
isCrossRepository: false,
__typename: 'ConnectedEvent',
subject: { number: 2 },
},
],
},
},
},
{
node: {
bodyHTML: '',
number: 42,
labels: {
pageInfo: { hasNextPage: false },
nodes: [{ name: 'label1' }, { name: 'skip' }],
},
timelineItems: {
pageInfo: { hasNextPage: false },
nodes: [
{
isCrossRepository: true,
__typename: 'ConnectedEvent',
subject: { number: 82 },
},
],
},
},
},
],
},
},
}),
),
}
jest.isolateModules(() => {
require('./index')
})
await new Promise<void>(setImmediate)
expect(mockOctokit).toMatchSnapshot()
expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledTimes(3)
})
describe('can filter tags', () => {
const v3prev = 'v3.0.1'
const v3current = 'v3.0.2'
const v2prev = 'v2.0.1'
const v2current = 'v2.0.2'
const listReleasesData = [
{
name: 'Current Release Name',
tag_name: v3current,
html_url: 'http://v3.0.2',
},
{
name: 'Prev Release Name',
tag_name: v3prev,
html_url: 'http://v3.0.1',
},
{
name: 'v2 Current Release Name',
tag_name: v2current,
html_url: 'http://v2.0.2',
},
{
name: 'v2 Prev Release Name',
tag_name: v2prev,
html_url: 'http://v2.0.1',
},
]
it.each`
description | prevTag | currentTag | filter
${'no filter'} | ${v3prev} | ${v3current} | ${null}
${'v3'} | ${v3prev} | ${v3current} | ${'v\\d'}
${'v2'} | ${v2prev} | ${v2current} | ${'v\\d'}
`('should filter tags with $description', async ({ prevTag, currentTag, filter }) => {
// @ts-ignore
github.context.payload.release.tag_name = currentTag
tagFilter = filter
mockOctokit = {
...simpleMockOctokit,
rest: {
issues: {
get: jest.fn(() => Promise.resolve({ data: { locked: false } })),
createComment: jest.fn(() => Promise.resolve()),
addLabels: jest.fn(() => Promise.resolve()),
},
repos: {
listReleases: jest.fn(() =>
Promise.resolve({
data: listReleasesData,
}),
),
compareCommits: jest.fn(() =>
Promise.resolve({
data: { commits: [{ sha: 'SHA1' }] },
}),
),
},
},
graphql: jest.fn(() =>
Promise.resolve({
resource: {
messageHeadlineHTML: '',
messageBodyHTML:
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
associatedPullRequests: {
pageInfo: { hasNextPage: false },
edges: [],
},
},
}),
),
}
jest.isolateModules(() => {
require('./index')
})
await new Promise<void>((resolve) => setImmediate(() => resolve()))
expect(github.getOctokit).toHaveBeenCalled()
expect(mockOctokit.rest.repos.compareCommits.mock.calls).toEqual([
[{ base: prevTag, head: currentTag }],
])
})
})
describe('feature tests', () => {
beforeEach(() => {
mockOctokit = simpleMockOctokit
})
it('can disable comments', async () => {
commentTempate = ''
jest.isolateModules(() => {
require('./index')
})
await new Promise<void>((resolve) => setImmediate(() => resolve()))
expect(github.getOctokit).toHaveBeenCalled()
expect(mockOctokit.rest.issues.createComment).not.toHaveBeenCalled()
})
it('should unlock and comment', async () => {
mockOctokit = {
...simpleMockOctokit,
rest: {
...simpleMockOctokit.rest,
issues: {
// Return locked for both issues to be commented on
get: jest.fn(() => Promise.resolve({ data: { locked: true } })),
lock: jest.fn(() => Promise.resolve()),
unlock: jest.fn(() => Promise.resolve()),
createComment: jest.fn(() => Promise.resolve()),
},
},
graphql: jest.fn(() =>
Promise.resolve({
resource: {
messageHeadlineHTML: '',
messageBodyHTML:
'<span class="issue-keyword tooltipped tooltipped-se" aria-label="This commit closes issue #123.">Closes</span> <p><span class="issue-keyword tooltipped tooltipped-se" aria-label="This pull request closes issue #7.">Closes</span>',
associatedPullRequests: {
pageInfo: { hasNextPage: false },
edges: [],
},
},
}),
),
}
jest.isolateModules(() => {
require('./index')
})
await new Promise<void>((resolve) => setImmediate(() => resolve()))
expect(github.getOctokit).toHaveBeenCalled()
// Should call once for both linked issues
expect(mockOctokit.rest.issues.unlock).toHaveBeenCalledTimes(2)
expect(mockOctokit.rest.issues.createComment).toHaveBeenCalledTimes(2)
expect(mockOctokit.rest.issues.lock).toHaveBeenCalledTimes(2)
})
it.skip('can apply labels', async () => {
labelTemplate = ':dart: landed,release-{release_tag},{release_name}'
jest.isolateModules(() => {
require('./index')
})
await new Promise<void>((resolve) => setImmediate(() => resolve()))
expect(github.getOctokit).toHaveBeenCalled()
expect(mockOctokit.rest.issues.addLabels.mock.calls).toMatchSnapshot()
})
})
})

View File

@@ -1,349 +0,0 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import type * as Webhooks from '@octokit/webhooks-types'
const closesMatcher = /aria-label="This (?:commit|pull request) closes issue #(\d+)\."/g
const releaseLinkTemplateRegex = /{release_link}/g
const releaseNameTemplateRegex = /{release_name}/g
const releaseTagTemplateRegex = /{release_tag}/g
;(async function main() {
try {
const payload = github.context.payload as Webhooks.EventPayloadMap['release']
const githubToken = core.getInput('GITHUB_TOKEN')
const tagFilter = core.getInput('tag-filter') || undefined // Accept tag filter as an input
const octokit = github.getOctokit(githubToken)
const commentTemplate = core.getInput('comment-template')
const labelTemplate = core.getInput('label-template') || null
const skipLabelTemplate = core.getInput('skip-label') || null
// Fetch the releases with the optional tag filter applied
const { data: rawReleases } = await octokit.rest.repos.listReleases({
...github.context.repo,
per_page: 100,
})
// Get the current release tag or latest tag
const currentTag = payload?.release?.tag_name || rawReleases?.[0]?.tag_name
let releases = rawReleases
// Filter releases by the tag filter if provided
if (tagFilter) {
core.info(`Filtering releases by tag filter: ${tagFilter}`)
// Get the matching part of the current release tag
const regexMatch = currentTag.match(tagFilter)?.[0]
if (!regexMatch) {
core.error(`Current release tag ${currentTag} does not match the tag filter ${tagFilter}`)
return
}
core.info(`Matched string from filter: ${regexMatch}`)
releases = releases
.filter((release) => {
const match = release.tag_name.match(regexMatch)?.[0]
return match
})
.slice(0, 2)
}
core.info(`Releases: ${JSON.stringify(releases, null, 2)}`)
if (releases.length < 2) {
if (!releases.length) {
core.error(`No releases found with the provided tag filter: '${tagFilter}'`)
return
}
core.info('first release')
return
}
const [currentRelease, priorRelease] = releases
core.info(`${priorRelease.tag_name}...${currentRelease.tag_name}`)
const {
data: { commits },
} = await octokit.rest.repos.compareCommits({
...github.context.repo,
base: priorRelease.tag_name,
head: currentRelease.tag_name,
})
if (!currentRelease.name) {
core.info('Current release has no name, will fall back to the tag name.')
}
const releaseLabel = currentRelease.name || currentRelease.tag_name
const comment = commentTemplate
.trim()
.split(releaseLinkTemplateRegex)
.join(`[${releaseLabel}](${currentRelease.html_url})`)
.split(releaseNameTemplateRegex)
.join(releaseLabel)
.split(releaseTagTemplateRegex)
.join(currentRelease.tag_name)
const parseLabels = (rawInput: string | null) =>
rawInput
?.split(releaseNameTemplateRegex)
.join(releaseLabel)
?.split(releaseTagTemplateRegex)
.join(currentRelease.tag_name)
?.split(',')
?.map((l) => l.trim())
.filter((l) => l)
const labels = parseLabels(labelTemplate)
const skipLabels = parseLabels(skipLabelTemplate)
const linkedIssuesPrs = new Set<number>()
await Promise.all(
commits.map((commit) =>
(async () => {
const query = `
{
resource(url: "${payload.repository.html_url}/commit/${commit.sha}") {
... on Commit {
messageHeadlineHTML
messageBodyHTML
associatedPullRequests(first: 10) {
pageInfo {
hasNextPage
}
edges {
node {
bodyHTML
number
state
labels(first: 10) {
pageInfo {
hasNextPage
}
nodes {
name
}
}
timelineItems(itemTypes: [CONNECTED_EVENT, DISCONNECTED_EVENT], first: 100) {
pageInfo {
hasNextPage
}
nodes {
... on ConnectedEvent {
__typename
isCrossRepository
subject {
... on Issue {
number
}
}
}
... on DisconnectedEvent {
__typename
isCrossRepository
subject {
... on Issue {
number
}
}
}
}
}
}
}
}
}
}
}
`
const response: {
resource: null | {
messageHeadlineHTML: string
messageBodyHTML: string
associatedPullRequests: {
pageInfo: { hasNextPage: boolean }
edges: ReadonlyArray<{
node: {
bodyHTML: string
number: number
state: 'OPEN' | 'CLOSED' | 'MERGED'
labels: {
pageInfo: { hasNextPage: boolean }
nodes: ReadonlyArray<{
name: string
}>
}
timelineItems: {
pageInfo: { hasNextPage: boolean }
nodes: ReadonlyArray<{
__typename: 'ConnectedEvent' | 'DisconnectedEvent'
isCrossRepository: boolean
subject: {
number: number
}
}>
}
}
}>
}
}
} = await octokit.graphql(query)
if (!response.resource) {
return
}
// core.info(JSON.stringify(response.resource, null, 2))
core.info(`Checking commit: ${payload.repository.html_url}/commit/${commit.sha}`)
const associatedClosedPREdges = response.resource.associatedPullRequests.edges.filter(
(e) => e.node.state === 'MERGED',
)
if (associatedClosedPREdges.length) {
core.info(
` Associated Merged PRs:\n ${associatedClosedPREdges.map((pr) => `${payload.repository.html_url}/pull/${pr.node.number}`).join('\n ')}`,
)
} else {
core.info(' No associated merged PRs')
}
const html = [
response.resource.messageHeadlineHTML,
response.resource.messageBodyHTML,
...associatedClosedPREdges.map((pr) => pr.node.bodyHTML),
].join(' ')
for (const match of html.matchAll(closesMatcher)) {
const [, num] = match
linkedIssuesPrs.add(parseInt(num, 10))
core.info(
` Linked issue/PR from closesMatcher: ${payload.repository.html_url}/pull/${num}`,
)
}
if (response.resource.associatedPullRequests.pageInfo.hasNextPage) {
core.warning(`Too many PRs associated with ${commit.sha}`)
}
const seen = new Set<number>()
for (const associatedPR of associatedClosedPREdges) {
if (associatedPR.node.timelineItems.pageInfo.hasNextPage) {
core.warning(`Too many links for #${associatedPR.node.number}`)
}
if (associatedPR.node.labels.pageInfo.hasNextPage) {
core.warning(`Too many labels for #${associatedPR.node.number}`)
}
// a skip labels is present on this PR
if (
skipLabels?.some((l) => associatedPR.node.labels.nodes.some(({ name }) => name === l))
) {
continue
}
linkedIssuesPrs.add(associatedPR.node.number)
core.info(
` Linked issue/PR from associated PR: ${payload.repository.html_url}/pull/${associatedPR.node.number}`,
)
// These are sorted by creation date in ascending order. The latest event for a given issue/PR is all we need
// ignore links that aren't part of this repo
const links = associatedPR.node.timelineItems.nodes
.filter((node) => !node.isCrossRepository)
.reverse()
for (const link of links) {
if (seen.has(link.subject.number)) {
continue
}
if (link.__typename == 'ConnectedEvent') {
linkedIssuesPrs.add(link.subject.number)
core.info(
`Linked issue/PR from connected event: ${payload.repository.html_url}/pull/${link.subject.number}`,
)
}
seen.add(link.subject.number)
}
}
})(),
),
)
core.info(
`Final issues/PRs to be commented on: \n${Array.from(linkedIssuesPrs)
.map((num) => ` ${payload.repository.html_url}/pull/${num}`)
.join('\n')}`,
)
const requests: Array<Promise<unknown>> = []
for (const issueNumber of linkedIssuesPrs) {
const baseRequest = {
...github.context.repo,
issue_number: issueNumber,
}
if (comment) {
const commentRequest = {
...baseRequest,
body: comment,
}
// Check if issue is locked or not
const { data: issue } = await octokit.rest.issues.get(baseRequest)
let createCommentPromise: () => Promise<void>
if (!issue.locked) {
createCommentPromise = async () => {
try {
await octokit.rest.issues.createComment(commentRequest)
} catch (error) {
core.error(error as Error)
core.error(
`Failed to comment on issue/PR: ${issueNumber}. ${payload.repository.html_url}/pull/${issueNumber}`,
)
}
}
} else {
core.info(
`Issue/PR is locked: ${issueNumber}. Unlocking, commenting, and re-locking. ${payload.repository.html_url}/pull/${issueNumber}`,
)
createCommentPromise = async () => {
try {
core.debug(`Unlocking issue/PR: ${issueNumber}`)
await octokit.rest.issues.unlock(baseRequest)
core.debug(`Commenting on issue/PR: ${issueNumber}`)
await octokit.rest.issues.createComment(commentRequest)
core.debug(`Re-locking issue/PR: ${issueNumber}`)
await octokit.rest.issues.lock(baseRequest)
} catch (error) {
core.error(error as Error)
core.error(
`Failed to unlock, comment, and re-lock issue/PR: ${issueNumber}. ${payload.repository.html_url}/pull/${issueNumber}`,
)
}
}
}
requests.push(createCommentPromise())
}
if (labels) {
const request = {
...baseRequest,
labels,
}
// core.info(JSON.stringify(request, null, 2))
requests.push(octokit.rest.issues.addLabels(request))
}
}
await Promise.all(requests)
} catch (error) {
core.error(error as Error)
core.setFailed((error as Error).message)
}
})()

View File

@@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["es2020.string"],
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"downlevelIteration": true,
"skipLibCheck": true,
},
"exclude": ["src/**/*.test.ts"]
}

View File

@@ -1,76 +0,0 @@
name: Setup node and pnpm
description: |
Configures Node, pnpm, cache, performs pnpm install
inputs:
node-version:
description: Node.js version
required: true
default: 22.6.0
pnpm-version:
description: Pnpm version
required: true
default: 9.7.1
pnpm-run-install:
description: Whether to run pnpm install
required: false
default: true
pnpm-restore-cache:
description: Whether to restore cache
required: false
default: true
pnpm-install-cache-key:
description: The cache key for the pnpm install cache
default: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
outputs:
pnpm-store-path:
description: The resolved pnpm store path
pnpm-install-cache-key:
description: The cache key used for pnpm install cache
runs:
using: composite
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
shell: bash
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ inputs.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ inputs.pnpm-version }}
run_install: false
- name: Get pnpm store path
shell: bash
run: |
STORE_PATH=$(pnpm store path --silent)
echo "STORE_PATH=$STORE_PATH" >> $GITHUB_ENV
echo "Pnpm store path resolved to: $STORE_PATH"
- name: Restore pnpm install cache
if: ${{ inputs.pnpm-restore-cache == 'true' }}
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ inputs.pnpm-install-cache-key }}
restore-keys: |
pnpm-store-${{ inputs.pnpm-version }}-
pnpm-store-
- name: Run pnpm install
if: ${{ inputs.pnpm-run-install == 'true' }}
shell: bash
run: pnpm install
# Set the cache key output
- run: |
echo "pnpm-install-cache-key=${{ inputs.pnpm-install-cache-key }}" >> $GITHUB_ENV
shell: bash

View File

@@ -1,22 +0,0 @@
MIT License
Copyright (c) 2024 Payload <info@payloadcms.com>. All modification and additions are copyright of Payload.
---
Original license:
ISC License
Copyright (c) 2023, Balázs Orbán
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -1,21 +0,0 @@
# Triage
Modified version of https://github.com/balazsorban44/nissuer
## Modifications
- Port to TypeScript
- Remove issue locking
- Remove reproduction blocklist
- Uses `@vercel/ncc` for packaging
## Development
> [!IMPORTANT]
> Whenever a modification is made to the action, the action built to `dist` must be committed to the repository.
This is done by running:
```sh
pnpm build
```

View File

@@ -1,40 +0,0 @@
name: Triage
description: Initial triage for issues
inputs:
reproduction-comment:
description: 'Either a string or a path to a .md file inside the repository. Example: ".github/invalid-reproduction.md"'
default: '.github/invalid-reproduction.md'
reproduction-hosts:
description: 'Comma-separated list of hostnames that are allowed for reproductions. Example: "github.com,codesandbox.io"'
default: github.com
reproduction-invalid-label:
description: 'Label to apply to issues without a valid reproduction. Example: "invalid-reproduction"'
default: 'invalid-reproduction'
reproduction-issue-labels:
description: 'Comma-separated list of issue labels. If configured, only verify reproduction URLs of issues with one of these labels present. Adding a comma at the end will handle non-labeled issues as invalid. Example: "bug,", will consider issues with the label "bug" or no label.'
default: ''
reproduction-link-section:
description: 'A regular expression string with "(.*)" matching a valid URL in the issue body. The result is trimmed. Example: "### Link to reproduction(.*)### To reproduce"'
default: '### Link to reproduction(.*)### To reproduce'
actions-to-perform:
description: 'Comma-separated list of actions to perform on the issue. Example: "tag,comment,close"'
default: 'tag,comment,close'
runs:
using: 'composite'
steps:
- name: Checkout code
if: ${{ github.event_name != 'pull_request' }}
uses: actions/checkout@v4
- name: Run action
run: node ${{ github.action_path }}/dist/index.js
shell: sh
# https://github.com/actions/runner/issues/665#issuecomment-676581170
env:
'INPUT_REPRODUCTION_COMMENT': ${{inputs.reproduction-comment}}
'INPUT_REPRODUCTION_HOSTS': ${{inputs.reproduction-hosts}}
'INPUT_REPRODUCTION_INVALID_LABEL': ${{inputs.reproduction-invalid-label}}
'INPUT_REPRODUCTION_ISSUE_LABELS': ${{inputs.reproduction-issue-labels}}
'INPUT_REPRODUCTION_LINK_SECTION': ${{inputs.reproduction-link-section}}
'INPUT_ACTIONS_TO_PERFORM': ${{inputs.actions-to-perform}}

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
module.exports = {
testEnvironment: 'node',
testPathIgnorePatterns: ['/node_modules/', '<rootDir>/dist/'],
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest'],
},
}

View File

@@ -1,33 +0,0 @@
{
"name": "triage",
"version": "0.0.0",
"private": true,
"description": "GitHub Action to triage new issues",
"license": "MIT",
"main": "dist/index.js",
"scripts": {
"build": "pnpm build:typecheck && pnpm build:ncc",
"build:ncc": "ncc build src/index.ts -t -o dist",
"build:typecheck": "tsc",
"clean": "rimraf dist",
"test": "jest"
},
"dependencies": {
"@actions/core": "^1.3.0",
"@actions/github": "^5.0.0"
},
"devDependencies": {
"@octokit/webhooks-types": "^7.5.1",
"@swc/jest": "^0.2.36",
"@types/jest": "^27.5.2",
"@types/node": "^20.16.5",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"@vercel/ncc": "0.38.1",
"concurrently": "^8.2.2",
"eslint": "^7.32.0",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"typescript": "^4.9.5"
}
}

4241
.github/actions/triage/pnpm-lock.yaml generated vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,215 +0,0 @@
import { debug, error, getBooleanInput, getInput, info, setFailed } from '@actions/core'
import { context, getOctokit } from '@actions/github'
import { readFile, access } from 'node:fs/promises'
import { join } from 'node:path'
// Ensure GITHUB_TOKEN and GITHUB_WORKSPACE are present
if (!process.env.GITHUB_TOKEN) throw new TypeError('No GITHUB_TOKEN provided')
if (!process.env.GITHUB_WORKSPACE) throw new TypeError('Not a GitHub workspace')
const validActionsToPerform = ['tag', 'comment', 'close'] as const
type ActionsToPerform = (typeof validActionsToPerform)[number]
// Define the configuration object
interface Config {
invalidLink: {
comment: string
bugLabels: string[]
hosts: string[]
label: string
linkSection: string
}
actionsToPerform: ActionsToPerform[]
token: string
workspace: string
}
const config: Config = {
invalidLink: {
comment: getInput('reproduction_comment') || '.github/invalid-reproduction.md',
bugLabels: getInput('reproduction_issue_labels')
.split(',')
.map((l) => l.trim()),
hosts: (getInput('reproduction_hosts') || 'github.com').split(',').map((h) => h.trim()),
label: getInput('reproduction_invalid_label') || 'invalid-reproduction',
linkSection:
getInput('reproduction_link_section') || '### Link to reproduction(.*)### To reproduce',
},
actionsToPerform: (getInput('actions_to_perform') || validActionsToPerform.join(','))
.split(',')
.map((a) => {
const action = a.trim().toLowerCase() as ActionsToPerform
if (validActionsToPerform.includes(action)) {
return action
}
throw new TypeError(`Invalid action: ${action}`)
}),
token: process.env.GITHUB_TOKEN,
workspace: process.env.GITHUB_WORKSPACE,
}
// Attempt to parse JSON, return parsed object or error
function tryParse(json: string): Record<string, unknown> {
try {
return JSON.parse(json)
} catch (e) {
setFailed(`Could not parse JSON: ${e instanceof Error ? e.message : e}`)
return {}
}
}
// Retrieves a boolean input or undefined based on environment variables
function getBooleanOrUndefined(value: string): boolean | undefined {
const variable = process.env[`INPUT_${value.toUpperCase()}`]
return variable === undefined || variable === '' ? undefined : getBooleanInput(value)
}
// Returns the appropriate label match type
function getLabelMatch(value: string | undefined): 'name' | 'description' {
return value === 'name' ? 'name' : 'description'
}
// Function to check if an issue contains a valid reproduction link
async function checkValidReproduction(): Promise<void> {
const { issue, action } = context.payload as {
issue: { number: number; body: string; labels: { name: string }[] } | undefined
action: string
}
if (action !== 'opened' || !issue?.body) return
const labels = issue.labels.map((l) => l.name)
const issueMatchingLabel =
labels.length &&
config.invalidLink.bugLabels.length &&
labels.some((l) => config.invalidLink.bugLabels.includes(l))
if (!issueMatchingLabel) {
info(
`Issue #${issue.number} does not match required labels: ${config.invalidLink.bugLabels.join(', ')}`,
)
info(`Issue labels: ${labels.join(', ')}`)
return
}
info(`Issue #${issue.number} labels: ${labels.join(', ')}`)
const { rest: client } = getOctokit(config.token)
const common = { ...context.repo, issue_number: issue.number }
const labelsToRemove = labels.filter((l) => config.invalidLink.bugLabels.includes(l))
if (await isValidReproduction(issue.body)) {
await Promise.all(
labelsToRemove.map((label) => client.issues.removeLabel({ ...common, name: label })),
)
return info(`Issue #${issue.number} contains a valid reproduction 💚`)
}
info(`Invalid reproduction, issue will be closed/labeled/commented...`)
// Adjust labels
await Promise.all(
labelsToRemove.map((label) => client.issues.removeLabel({ ...common, name: label })),
)
// Tag
if (config.actionsToPerform.includes('tag')) {
info(`Added label: ${config.invalidLink.label}`)
await client.issues.addLabels({ ...common, labels: [config.invalidLink.label] })
} else {
info('Tag - skipped, not provided in actions to perform')
}
// Comment
if (config.actionsToPerform.includes('comment')) {
const comment = join(config.workspace, config.invalidLink.comment)
await client.issues.createComment({ ...common, body: await getCommentBody(comment) })
info(`Commented with invalid reproduction message`)
} else {
info('Comment - skipped, not provided in actions to perform')
}
// Close
if (config.actionsToPerform.includes('close')) {
await client.issues.update({ ...common, state: 'closed' })
info(`Closed issue #${issue.number}`)
} else {
info('Close - skipped, not provided in actions to perform')
}
}
/**
* Determine if an issue contains a valid/accessible link to a reproduction.
*
* Returns `true` if the link is valid.
* @param body - The body content of the issue
*/
async function isValidReproduction(body: string): Promise<boolean> {
const linkSectionRe = new RegExp(config.invalidLink.linkSection, 'is')
const link = body.match(linkSectionRe)?.[1]?.trim()
if (!link) {
info('Missing link')
info(`Link section regex: ${linkSectionRe}`)
info(`Link section: ${body}`)
return false
}
info(`Checking validity of link: ${link}`)
if (!URL.canParse(link)) {
info(`Invalid URL: ${link}`)
return false
}
const url = new URL(link)
if (!config.invalidLink.hosts.includes(url.hostname)) {
info('Link did not match allowed reproduction hosts')
return false
}
try {
// Verify that the link can be accessed
const response = await fetch(link)
const isOk = response.status < 400 || response.status >= 500
info(`Link status: ${response.status}`)
if (!isOk) {
info(`Link returned status ${response.status}`)
}
return isOk
} catch (error) {
info(`Error fetching link: ${(error as Error).message}`)
return false
}
}
/**
* Return either a file's content or a string
* @param {string} pathOrComment
*/
async function getCommentBody(pathOrComment: string) {
try {
await access(pathOrComment)
return await readFile(pathOrComment, 'utf8')
} catch (error: any) {
if (error.code === 'ENOENT') return pathOrComment
throw error
}
}
async function run() {
const { token, workspace, ...safeConfig } = config
info('Configuration:')
info(JSON.stringify(safeConfig, null, 2))
await checkValidReproduction()
}
run().catch(setFailed)

View File

@@ -1,15 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["es2020.string"],
"noEmit": true,
"strict": true,
"noUnusedLocals": false, // Undo this
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"downlevelIteration": true,
"skipLibCheck": true,
},
"exclude": ["src/**/*.test.ts"]
}

View File

@@ -1,20 +0,0 @@
**Please add a reproduction in order for us to be able to investigate.**
Depending on the quality of reproduction steps, this issue may be closed if no reproduction is provided.
### Why was this issue marked with the `invalid-reproduction` label?
To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with `create-payload-app@beta -t blank` or a forked/branched version of this repository with tests added (more info in the [reproduction-guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)).
To make sure the issue is resolved as quickly as possible, please make sure that the reproduction is as **minimal** as possible. This means that you should **remove unnecessary code, files, and dependencies** that do not contribute to the issue. Ensure your reproduction does not depend on secrets, 3rd party registries, private dependencies, or any other data that cannot be made public. Avoid a reproduction including a whole monorepo (unless relevant to the issue). The easier it is to reproduce the issue, the quicker we can help.
Please test your reproduction against the latest version of Payload to make sure your issue has not already been fixed.
### I added a link, why was it still marked?
Ensure the link is pointing to a codebase that is accessible (e.g. not a private repository). "[example.com](http://example.com/)", "n/a", "will add later", etc. are not acceptable links -- we need to see a public codebase. See the above section for accepted links.
### Useful Resources
- [Reproduction Guide](https://github.com/payloadcms/payload/blob/main/.github/reproduction-guide.md)
- [Contributing to Payload](https://www.youtube.com/watch?v=08Qa3ggR9rw)

View File

@@ -1,69 +0,0 @@
# docs: https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: github-actions
directories:
- /
- /.github/workflows
- /.github/actions/* # Not working until resolved: https://github.com/dependabot/dependabot-core/issues/6345
- /.github/actions/setup
target-branch: main
schedule:
interval: monthly
timezone: America/Detroit
time: '06:00'
groups:
github_actions:
patterns:
- '*'
- package-ecosystem: npm
directory: /
target-branch: main
open-pull-requests-limit: 0 # Only allow security updates
schedule:
interval: weekly
day: sunday
timezone: America/Detroit
time: '06:00'
commit-message:
prefix: 'chore(deps)'
labels:
- dependencies
groups:
production-deps:
dependency-type: production
update-types:
- minor
- patch
patterns:
- '*'
dev-deps:
dependency-type: development
update-types:
- minor
- patch
patterns:
- '*'
- package-ecosystem: npm
directory: /
target-branch: 2.x
open-pull-requests-limit: 0 # Only allow security updates
schedule:
interval: weekly
day: sunday
timezone: America/Detroit
time: '06:00'
commit-message:
prefix: 'chore(deps)'
labels:
- dependencies
groups:
production-deps:
dependency-type: production
update-types:
- patch
patterns:
- '*'

3976
.github/pnpm-lock.yaml generated vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
packages:
- 'actions/*'

View File

@@ -1,64 +0,0 @@
# Reproduction Guide
1. [Fork](https://github.com/payloadcms/payload/fork) this repo
2. Optionally, create a new branch for your reproduction
3. Run `pnpm install` to install dependencies
4. Open up the `test/_community` directory
5. Add any necessary `collections/globals/fields` in this directory to recreate the issue you are experiencing
6. Run `pnpm dev _community` to start the admin panel
**NOTE:** The goal is to isolate the problem by reducing the number of `collections/globals/fields` you add to the `test/_community` folder. This folder is _not_ meant for you to copy your project into, but rather recreate the issue you are experiencing with minimal config.
## Example test directory file tree
```text
.
├── config.ts
├── int.spec.ts
├── e2e.spec.ts
└── payload-types.ts
```
- `config.ts` - This is the _granular_ Payload config for testing. It should be as lightweight as possible. Reference existing configs for an example
- `int.spec.ts` [Optional] - This is the test file run by jest. Any test file must have a `*int.spec.ts` suffix.
- `e2e.spec.ts` [Optional] - This is the end-to-end test file that will load up the admin UI using the above config and run Playwright tests.
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `pnpm dev:generate-types _community`.
The directory split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config. You should modify the files in `test/_community` to get started.
<br />
## Testing is optional but encouraged
An issue does not need to have failing tests — reproduction steps with your forked repo are enough at this point. Some people like to dive deeper and we want to give you the guidance/tools to do so. Read more below:
### Running integration tests (Payload API tests)
First install [Jest Runner for VSVode](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner).
There are a couple ways run integration tests:
- **Granularly** - you can run individual tests in vscode by installing the Jest Runner plugin and using that to run individual tests. Clicking the `debug` button will run the test in debug mode allowing you to set break points.
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/assets/images/github/int-debug.png" />
- **Manually** - you can run all int tests in the `/test/_community/int.spec.ts` file by running the following command:
```bash
pnpm test:int _community
```
### Running E2E tests (Admin Panel UI tests)
The easiest way to run E2E tests is to install
- [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright)
- [Playwright Runner](https://marketplace.visualstudio.com/items?itemName=ortoni.ortoni)
Once they are installed you can open the `testing` tab in vscode sidebar and drill down to the test you want to run, i.e. `/test/_community/e2e.spec.ts`
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/assets/images/github/e2e-debug.png" />
#### Notes
The default credentials are `dev@payloadcms.com` as email and `test` as password. They can be found in `test/credentials.ts`. By default, these will be autofilled, so no log-in is required.

View File

@@ -1,24 +0,0 @@
name: dispatch-event
on:
workflow_dispatch:
env:
PAYLOAD_PUSH_MAIN_EVENT: payload-push-main-event
jobs:
repository-dispatch:
name: Repository dispatch
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Dispatch event
if: ${{ github.event_name == 'workflow_dispatch' }}
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PAYLOAD_REPOSITORY_DISPATCH }}
repository: ${{ secrets.REMOTE_REPOSITORY }}
event-type: ${{ env.PAYLOAD_PUSH_MAIN_EVENT }}
client-payload: '{"event": {"head_commit": {"id": "${{ env.GITHUB_SHA }}"}}}' # mocked for testing
# client-payload: '{"event": ${{ toJson(github.event) }}}'

View File

@@ -1,116 +0,0 @@
name: label-on-change
on:
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
issues:
types:
- assigned
- closed
- labeled
- reopened
# TODO: Handle labeling on comment
jobs:
on-labeled-ensure-one-status:
runs-on: ubuntu-24.04
permissions:
issues: write
# Only run on issue labeled and if label starts with 'status:'
if: github.event.action == 'labeled' && startsWith(github.event.label.name, 'status:')
steps:
- name: Ensure only one status label
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// Get all labels that start with 'status:' and are not the incoming label
const incomingLabelName = context.payload.label.name;
const labelNamesToRemove = context.payload.issue.labels
.filter(label => label.name.startsWith('status:') && label.name !== incomingLabelName)
.map(label => label.name);
if (!labelNamesToRemove.length) {
console.log('No labels to remove');
return;
}
console.log(`Labels to remove: '${labelNamesToRemove}'`);
// If there is more than one status label, remove all but the incoming label
for (const labelName of labelNamesToRemove) {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
name: labelName,
owner: context.repo.owner,
repo: context.repo.repo,
});
console.log(`Removed '${labelName}' label`);
}
on-issue-close:
runs-on: ubuntu-24.04
permissions:
issues: write
if: github.event.action == 'closed'
steps:
- name: Remove all labels on issue close
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// Get all labels that start with 'status:' and 'stale'
const labelNamesToRemove = context.payload.issue.labels
.filter(label => label.name.startsWith('status:') || label.name === 'stale')
.map(label => label.name);
if (!labelNamesToRemove.length) {
console.log('No labels to remove');
return;
}
console.log(`Labels to remove: '${labelNamesToRemove}'`);
for (const labelName of labelNamesToRemove) {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
name: labelName,
owner: context.repo.owner,
repo: context.repo.repo,
});
console.log(`Removed '${labelName}' label`);
}
on-issue-reopen:
runs-on: ubuntu-24.04
permissions:
issues: write
if: github.event.action == 'reopened'
steps:
- name: Add needs-triage label on issue reopen
uses: actions-ecosystem/action-add-labels@v1
with:
labels: 'status: needs-triage'
on-issue-assigned:
runs-on: ubuntu-24.04
permissions:
issues: write
if: >
github.event.action == 'assigned' &&
contains(github.event.issue.labels.*.name, 'status: needs-triage')
steps:
- name: Remove needs-triage label on issue assign
uses: actions-ecosystem/action-remove-labels@v1
with:
labels: 'status: needs-triage'
# on-pr-merge:
# runs-on: ubuntu-24.04
# if: github.event.pull_request.merged == true
# steps:
# on-pr-close:
# runs-on: ubuntu-24.04
# if: github.event_name == 'pull_request_target' && github.event.pull_request.merged == false
# steps:

View File

@@ -1,26 +0,0 @@
name: lock-issues
on:
schedule:
# Run nightly at 12am EST
- cron: '0 4 * * *'
workflow_dispatch:
permissions:
issues: write
jobs:
lock_issues:
runs-on: ubuntu-24.04
steps:
- name: Lock issues
uses: dessant/lock-threads@v5
with:
process-only: 'issues'
issue-inactive-days: '1'
exclude-any-issue-labels: 'status: awaiting-reply'
log-output: true
issue-comment: >
This issue has been automatically locked.
Please open a new issue if this issue persists with any additional detail.

View File

@@ -1,521 +0,0 @@
name: build
on:
pull_request:
types:
- opened
- reopened
- synchronize
push:
branches:
- main
concurrency:
# <workflow_name>-<branch_name>-<true || commit_sha if branch is protected>
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.ref_protected && github.sha || ''}}
cancel-in-progress: true
env:
NODE_VERSION: 22.6.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
jobs:
changes:
runs-on: ubuntu-24.04
permissions:
pull-requests: read
outputs:
needs_build: ${{ steps.filter.outputs.needs_build }}
needs_tests: ${{ steps.filter.outputs.needs_tests }}
templates: ${{ steps.filter.outputs.templates }}
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
needs_build:
- '.github/workflows/**'
- 'packages/**'
- 'test/**'
- 'pnpm-lock.yaml'
- 'package.json'
- 'templates/**'
needs_tests:
- '.github/workflows/**'
- 'packages/**'
- 'test/**'
- 'pnpm-lock.yaml'
- 'package.json'
templates:
- 'templates/**'
- name: Log all filter results
run: |
echo "needs_build: ${{ steps.filter.outputs.needs_build }}"
echo "needs_tests: ${{ steps.filter.outputs.needs_tests }}"
echo "templates: ${{ steps.filter.outputs.templates }}"
lint:
# Follows same github's ci skip: [skip lint], [lint skip], [no lint]
if: >
github.event_name == 'pull_request' &&
!contains(github.event.pull_request.title, '[skip lint]') &&
!contains(github.event.pull_request.title, '[lint skip]') &&
!contains(github.event.pull_request.title, '[no lint]')
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Lint staged
run: |
git diff --name-only --diff-filter=d origin/${GITHUB_BASE_REF}...${GITHUB_SHA}
npx lint-staged --diff="origin/${GITHUB_BASE_REF}...${GITHUB_SHA}"
build:
needs: changes
if: ${{ needs.changes.outputs.needs_build == 'true' }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- run: pnpm run build:all
env:
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
- name: Cache build
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
tests-unit:
runs-on: ubuntu-24.04
needs: [changes, build]
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
steps:
- uses: actions/checkout@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Unit Tests
run: pnpm test:unit
env:
NODE_OPTIONS: --max-old-space-size=8096
tests-types:
runs-on: ubuntu-24.04
needs: [changes, build]
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
steps:
- uses: actions/checkout@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Types Tests
run: pnpm test:types --target '>=5.7'
env:
NODE_OPTIONS: --max-old-space-size=8096
tests-int:
runs-on: ubuntu-24.04
needs: [changes, build]
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
name: int-${{ matrix.database }}
strategy:
fail-fast: false
matrix:
database:
- mongodb
- postgres
- postgres-custom-schema
- postgres-uuid
- supabase
- sqlite
- sqlite-uuid
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: payloadtests
AWS_ENDPOINT_URL: http://127.0.0.1:4566
AWS_ACCESS_KEY_ID: localstack
AWS_SECRET_ACCESS_KEY: localstack
AWS_REGION: us-east-1
services:
postgres:
image: ${{ (startsWith(matrix.database, 'postgres') ) && 'postgis/postgis:16-3.4' || '' }}
env:
# must specify password for PG Docker container image, see: https://registry.hub.docker.com/_/postgres?tab=description&page=1&name=10
POSTGRES_USER: ${{ env.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
POSTGRES_DB: ${{ env.POSTGRES_DB }}
ports:
- 5432:5432
# needed because the postgres container does not provide a healthcheck
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Start LocalStack
run: pnpm docker:start
- name: Install Supabase CLI
uses: supabase/setup-cli@v1
with:
version: latest
if: matrix.database == 'supabase'
- name: Initialize Supabase
run: |
supabase init
supabase start
if: matrix.database == 'supabase'
- name: Configure PostgreSQL
run: |
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;"
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "SELECT version();"
echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
if: startsWith(matrix.database, 'postgres')
- name: Configure PostgreSQL with custom schema
run: |
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE SCHEMA custom;"
if: matrix.database == 'postgres-custom-schema'
- name: Configure Supabase
run: |
echo "POSTGRES_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres" >> $GITHUB_ENV
if: matrix.database == 'supabase'
- name: Integration Tests
run: pnpm test:int
env:
NODE_OPTIONS: --max-old-space-size=8096
PAYLOAD_DATABASE: ${{ matrix.database }}
POSTGRES_URL: ${{ env.POSTGRES_URL }}
tests-e2e:
runs-on: ubuntu-24.04
needs: [changes, build]
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
name: e2e-${{ matrix.suite }}
strategy:
fail-fast: false
matrix:
# find test -type f -name 'e2e.spec.ts' | sort | xargs dirname | xargs -I {} basename {}
suite:
- _community
- access-control
- admin__e2e__general
- admin__e2e__list-view
- admin__e2e__document-view
- admin-root
- auth
- auth-basic
- joins
- field-error-states
- fields-relationship
- fields__collections__Array
- fields__collections__Blocks
- fields__collections__Checkbox
- fields__collections__Collapsible
- fields__collections__ConditionalLogic
- fields__collections__CustomID
- fields__collections__Date
- fields__collections__Email
- fields__collections__Indexed
- fields__collections__JSON
- fields__collections__Lexical__e2e__main
- fields__collections__Lexical__e2e__blocks
- fields__collections__Number
- fields__collections__Point
- fields__collections__Radio
- fields__collections__Relationship
- fields__collections__RichText
- fields__collections__Row
- fields__collections__Select
- fields__collections__Tabs
- fields__collections__Tabs2
- fields__collections__Text
- fields__collections__UI
- fields__collections__Upload
- live-preview
- localization
- locked-documents
- i18n
- plugin-cloud-storage
- plugin-form-builder
- plugin-nested-docs
- plugin-seo
- versions
- uploads
env:
SUITE_NAME: ${{ matrix.suite }}
steps:
- uses: actions/checkout@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Start LocalStack
run: pnpm docker:start
if: ${{ matrix.suite == 'plugin-cloud-storage' }}
- name: Store Playwright's Version
run: |
# Extract the version number using a more targeted regex pattern with awk
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --depth=0 | awk '/@playwright\/test/ {print $2}')
echo "Playwright's Version: $PLAYWRIGHT_VERSION"
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV
- name: Cache Playwright Browsers for Playwright's Version
id: cache-playwright-browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ env.PLAYWRIGHT_VERSION }}
- name: Setup Playwright - Browsers and Dependencies
if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium
- name: Setup Playwright - Dependencies-only
if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps chromium
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.suite }}
path: test/test-results/
if-no-files-found: ignore
retention-days: 1
# Disabled until this is fixed: https://github.com/daun/playwright-report-summary/issues/156
# - uses: daun/playwright-report-summary@v3
# with:
# report-file: results_${{ matrix.suite }}.json
# report-tag: ${{ matrix.suite }}
# job-summary: true
# Build listed templates with packed local packages
build-templates:
runs-on: ubuntu-24.04
needs: build
strategy:
matrix:
include:
- template: blank
database: mongodb
- template: website
database: mongodb
- template: with-payload-cloud
database: mongodb
- template: with-vercel-mongodb
database: mongodb
# Postgres
- template: with-postgres
database: postgres
- template: with-vercel-postgres
database: postgres
- template: plugin
# Re-enable once PG conncection is figured out
# - template: with-vercel-website
# database: postgres
name: ${{ matrix.template }}-${{ matrix.database }}
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: payloadtests
steps:
- uses: actions/checkout@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Start PostgreSQL
uses: CasperWA/postgresql-action@v1.2
with:
postgresql version: '14' # See https://hub.docker.com/_/postgres for available versions
postgresql db: ${{ env.POSTGRES_DB }}
postgresql user: ${{ env.POSTGRES_USER }}
postgresql password: ${{ env.POSTGRES_PASSWORD }}
if: matrix.database == 'postgres'
- name: Wait for PostgreSQL
run: sleep 30
if: matrix.database == 'postgres'
- name: Configure PostgreSQL
run: |
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;"
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "SELECT version();"
echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
if: matrix.database == 'postgres'
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.11.0
with:
mongodb-version: 6.0
if: matrix.database == 'mongodb'
- name: Build Template
run: |
pnpm run script:pack --dest templates/${{ matrix.template }}
pnpm run script:build-template-with-local-pkgs ${{ matrix.template }} $POSTGRES_URL
env:
NODE_OPTIONS: --max-old-space-size=8096
tests-type-generation:
runs-on: ubuntu-24.04
needs: [changes, build]
if: ${{ needs.changes.outputs.needs_tests == 'true' }}
steps:
- uses: actions/checkout@v4
- name: Node setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
pnpm-run-install: false
pnpm-restore-cache: false # Full build is restored below
pnpm-install-cache-key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Restore build
uses: actions/cache@v4
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Generate Payload Types
run: pnpm dev:generate-types fields
- name: Generate GraphQL schema file
run: pnpm dev:generate-graphql-schema graphql-schema-gen
all-green:
name: All Green
if: always()
runs-on: ubuntu-24.04
needs:
- lint
- build
- build-templates
- tests-unit
- tests-int
- tests-e2e
- tests-types
- tests-type-generation
steps:
- if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
run: exit 1
publish-canary:
name: Publish Canary
runs-on: ubuntu-24.04
if: ${{ needs.all-green.result == 'success' && github.ref_name == 'main' }}
needs:
- all-green
steps:
# debug github.ref output
- run: |
echo github.ref: ${{ github.ref }}
echo isV3: ${{ github.ref == 'refs/heads/main' }}

View File

@@ -1,121 +0,0 @@
name: post-release-templates
on:
release:
types:
- published
workflow_dispatch:
env:
NODE_VERSION: 22.6.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
jobs:
wait_for_release:
runs-on: ubuntu-24.04
outputs:
release_tag: ${{ steps.determine_tag.outputs.release_tag }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
sparse-checkout: .github/workflows
- name: Determine Release Tag
id: determine_tag
run: |
if [ "${{ github.event_name }}" == "release" ]; then
echo "Using tag from release event: ${{ github.event.release.tag_name }}"
echo "release_tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
else
# pull latest tag from github, must match any version except v2. Should match v3, v4, v99, etc.
echo "Fetching latest tag from github..."
LATEST_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude 'v2*')
echo "Latest tag: $LATEST_TAG"
echo "release_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
fi
- name: Wait until latest versions resolve on npm registry
run: |
./.github/workflows/wait-until-package-version.sh payload ${{ steps.determine_tag.outputs.release_tag }}
./.github/workflows/wait-until-package-version.sh @payloadcms/translations ${{ steps.determine_tag.outputs.release_tag }}
./.github/workflows/wait-until-package-version.sh @payloadcms/next ${{ steps.determine_tag.outputs.release_tag }}
update_templates:
needs: wait_for_release
runs-on: ubuntu-24.04
permissions:
contents: write
pull-requests: write
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: payloadtests
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
- name: Start PostgreSQL
uses: CasperWA/postgresql-action@v1.2
with:
postgresql version: '14' # See https://hub.docker.com/_/postgres for available versions
postgresql db: ${{ env.POSTGRES_DB }}
postgresql user: ${{ env.POSTGRES_USER }}
postgresql password: ${{ env.POSTGRES_PASSWORD }}
- name: Wait for PostgreSQL
run: sleep 30
- name: Configure PostgreSQL
run: |
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;"
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "SELECT version();"
echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
echo "DATABASE_URI=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.11.0
with:
mongodb-version: 6.0
- name: Update template lockfiles and migrations
run: pnpm script:gen-templates
- name: Commit and push changes
id: commit
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -ex
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git diff --name-only
export BRANCH_NAME=templates/bump-${{ needs.wait_for_release.outputs.release_tag }}-$(date +%s)
echo "branch=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
- name: Create pull request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GH_TOKEN_POST_RELEASE_TEMPLATES }}
labels: 'area: templates'
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
commit-message: 'templates: bump templates for ${{ needs.wait_for_release.outputs.release_tag }}'
branch: ${{ steps.commit.outputs.branch }}
base: main
assignees: ${{ github.actor }}
title: 'templates: bump for ${{ needs.wait_for_release.outputs.release_tag }}'
body: |
🤖 Automated bump of templates for ${{ needs.wait_for_release.outputs.release_tag }}
Triggered by user: @${{ github.actor }}

View File

@@ -1,52 +0,0 @@
name: post-release
on:
release:
types:
- published
workflow_dispatch:
inputs:
tag:
description: 'Release tag to process (optional)'
required: false
default: ''
env:
NODE_VERSION: 22.6.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
jobs:
post_release:
runs-on: ubuntu-24.04
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/release-commenter
continue-on-error: true
env:
ACTIONS_STEP_DEBUG: true
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag-filter: 'v\d'
# Change to blank to disable commenting
# comment-template: ''
comment-template: |
🚀 This is included in version {release_link}
github-releases-to-discord:
runs-on: ubuntu-24.04
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Github Releases To Discord
uses: SethCohen/github-releases-to-discord@v1.16.2
with:
webhook_url: ${{ secrets.DISCORD_RELEASES_WEBHOOK_URL }}
color: '16777215'
username: 'Payload Releases'
avatar_url: 'https://l4wlsi8vxy8hre4v.public.blob.vercel-storage.com/discord-bot-logo.png'

View File

@@ -1,119 +0,0 @@
name: pr-title
on:
pull_request_target:
types:
- opened
- edited
permissions:
pull-requests: write
jobs:
main:
name: lint-pr-title
runs-on: ubuntu-24.04
steps:
- uses: amannn/action-semantic-pull-request@v5
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
build
chore
ci
docs
examples
feat
fix
perf
refactor
revert
style
templates
test
scopes: |
cpa
db-\*
db-mongodb
db-postgres
db-vercel-postgres
db-sqlite
drizzle
email-\*
email-nodemailer
email-resend
eslint
graphql
live-preview
live-preview-react
next
payload-cloud
plugin-cloud
plugin-cloud-storage
plugin-form-builder
plugin-nested-docs
plugin-redirects
plugin-search
plugin-sentry
plugin-seo
plugin-stripe
richtext-\*
richtext-lexical
richtext-slate
storage-\*
storage-azure
storage-gcs
storage-uploadthing
storage-vercel-blob
storage-s3
translations
ui
templates
examples(\/(\w|-)+)?
deps
# Disallow uppercase letters at the beginning of the subject
subjectPattern: ^(?![A-Z]).+$
subjectPatternError: |
The subject "{subject}" found in the pull request title "{title}"
didn't match the configured pattern. Please ensure that the subject
doesn't start with an uppercase character.
- uses: marocchino/sticky-pull-request-comment@v2
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Pull Request titles must follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and have valid scopes.
${{ steps.lint_pr_title.outputs.error_message }}
```
feat(ui): add Button component
^ ^ ^
| | |__ Subject
| |_______ Scope
|____________ Type
```
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
delete: true
label-pr-on-open:
name: label-pr-on-open
runs-on: ubuntu-24.04
if: github.event.action == 'opened'
steps:
- name: Tag with 2.x branch with v2
if: github.event.pull_request.base.ref == '2.x'
uses: actions-ecosystem/action-add-labels@v1
with:
labels: v2

View File

@@ -1,34 +0,0 @@
name: release-canary
on:
workflow_dispatch:
env:
NODE_VERSION: 22.6.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
jobs:
release:
name: release-canary-${{ github.ref_name }}-${{ github.sha }}
permissions:
id-token: write
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
with:
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
- name: Load npm token
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Canary release script
run: pnpm release:canary
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true

View File

@@ -1,51 +0,0 @@
name: stale
on:
schedule:
# Run nightly at 1am EST
- cron: '0 5 * * *'
workflow_dispatch:
inputs:
dry-run:
description: Run the stale action in debug-only mode
default: false
required: false
type: boolean
jobs:
stale:
runs-on: ubuntu-24.04
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
id: stale
with:
debug-only: ${{ inputs.dry-run || false }}
days-before-stale: 30
days-before-close: -1 # Disable closing
ascending: true
operations-per-run: 300
exempt-all-assignees: true
# Issues
stale-issue-label: stale
exempt-issue-labels: 'prioritized,keep,created-by: Payload team,created-by: Contributor,status: verified'
stale-issue-message: ''
close-issue-message: |
This issue was automatically closed due to lack of activity.
# Pull Requests
stale-pr-label: stale
exempt-pr-labels: 'prioritized,keep,created-by: Payload team,created-by: Contributor'
stale-pr-message: ''
close-pr-message: |
This pull request was automatically closed due to lack of activity.
# TODO: Add a step to notify team
- name: Print outputs
run: echo ${{ format('{0},{1}', toJSON(steps.stale.outputs.staled-issues-prs), toJSON(steps.stale.outputs.closed-issues-prs)) }}

View File

@@ -1,102 +0,0 @@
name: triage
on:
pull_request_target:
types:
- opened
issues:
types:
- opened
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: read
issues: write
pull-requests: write
jobs:
debug-context:
runs-on: ubuntu-24.04
steps:
- name: View context attributes
uses: actions/github-script@v7
with:
script: console.log({ context })
label-created-by:
name: label-on-open
runs-on: ubuntu-24.04
steps:
- name: Tag with 'created-by'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const payloadTeamUsernames = [
'denolfe',
'jmikrut',
'DanRibbens',
'jacobsfletch',
'JarrodMFlesch',
'AlessioGr',
'JessChowdhury',
'kendelljoseph',
'PatrikKozak',
'tylandavis',
'paulpopus',
'r1tsuu',
'GermanJablo',
];
const type = context.payload.pull_request ? 'pull_request' : 'issue';
const isTeamMember = payloadTeamUsernames
.map(n => n.toLowerCase())
.includes(context.payload[type].user.login.toLowerCase());
if (isTeamMember) {
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['created-by: Payload team'],
});
console.log(`Added 'created-by: Payload team' label`);
return;
}
const association = context.payload[type].author_association;
let label = ''
if (association === 'MEMBER' || association === 'OWNER') {
label = 'created-by: Payload team';
} else if (association === 'CONTRIBUTOR') {
label = 'created-by: Contributor';
}
if (!label) return;
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: [label],
});
console.log(`Added '${label}' label.`);
triage:
name: initial-triage
if: github.event_name == 'issues'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.base.ref }}
token: ${{ secrets.GITHUB_TOKEN }}
- uses: ./.github/actions/triage
with:
reproduction-comment: '.github/comments/invalid-reproduction.md'
reproduction-link-section: '### Link to the code that reproduces this issue(.*)### Reproduction Steps'
reproduction-issue-labels: 'validate-reproduction'
actions-to-perform: 'tag,comment'

View File

@@ -1,31 +0,0 @@
#!/bin/bash
if [[ "$#" -ne 2 ]]; then
echo "Usage: $0 <package-name> <version>"
exit 1
fi
PACKAGE_NAME="$1"
TARGET_VERSION=${2#v} # Git tag has leading 'v', npm version does not
TIMEOUT=300 # 5 minutes in seconds
INTERVAL=10 # 10 seconds
ELAPSED=0
echo "Waiting for version ${TARGET_VERSION} of '${PACKAGE_NAME}' to resolve... (timeout: ${TIMEOUT} seconds)"
while [[ ${ELAPSED} -lt ${TIMEOUT} ]]; do
latest_version=$(npm show "${PACKAGE_NAME}" version 2>/dev/null)
if [[ ${latest_version} == "${TARGET_VERSION}" ]]; then
echo "SUCCCESS: Version ${TARGET_VERSION} of ${PACKAGE_NAME} is available."
exit 0
else
echo "Version ${TARGET_VERSION} of ${PACKAGE_NAME} is not available yet. Retrying in ${INTERVAL} seconds... (elapsed: ${ELAPSED}s)"
fi
sleep "${INTERVAL}"
ELAPSED=$((ELAPSED + INTERVAL))
done
echo "Timed out after ${TIMEOUT} seconds waiting for version ${TARGET_VERSION} of '${PACKAGE_NAME}' to resolve."
exit 1

View File

@@ -1 +0,0 @@
pnpm run lint-staged --quiet

87
.idea/payload.iml generated
View File

@@ -1,87 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/packages/payload/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/payload/components" />
<excludeFolder url="file://$MODULE_DIR$/packages/payload/dist" />
<excludeFolder url="file://$MODULE_DIR$/.swc" />
<excludeFolder url="file://$MODULE_DIR$/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/examples" />
<excludeFolder url="file://$MODULE_DIR$/media" />
<excludeFolder url="file://$MODULE_DIR$/packages/create-payload-app/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/create-payload-app/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-mongodb/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-mongodb/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-postgres/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-postgres/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/graphql/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/graphql/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview-react/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview-react/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/next/.swc" />
<excludeFolder url="file://$MODULE_DIR$/packages/next/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/next/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/payload/fields" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud-storage/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud-storage/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-cloud/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-form-builder/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-nested-docs/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-nested-docs/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-redirects/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-redirects/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-search/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-search/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-sentry/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-seo/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-seo/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-stripe/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/richtext-lexical/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/richtext-lexical/dist" />
<excludeFolder url="file://$MODULE_DIR$/templates" />
<excludeFolder url="file://$MODULE_DIR$/test/.swc" />
<excludeFolder url="file://$MODULE_DIR$/versions" />
<excludeFolder url="file://$MODULE_DIR$/packages/richtext-slate/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/richtext-slate/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/email-nodemailer/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/email-nodemailer/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/email-resend/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/email-resend/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview-vue/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/live-preview-vue/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/payload/.swc" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-form-builder/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-relationship-object-ids/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-relationship-object-ids/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/plugin-stripe/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-azure/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-azure/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-gcs/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-gcs/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-s3/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-s3/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-uploadthing/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-uploadthing/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-vercel-blob/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/storage-vercel-blob/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/translations/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/translations/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/ui/.swc" />
<excludeFolder url="file://$MODULE_DIR$/packages/ui/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/ui/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/drizzle/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/drizzle/dist" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-sqlite/.turbo" />
<excludeFolder url="file://$MODULE_DIR$/packages/db-sqlite/dist" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -1,13 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev Fields" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<arguments value="fields" />
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@@ -1,13 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev _community" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<arguments value="_community" />
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@@ -1,13 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev admin" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<arguments value="admin" />
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>

View File

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

View File

@@ -1 +0,0 @@
v22.6.0

4
.npmrc
View File

@@ -1,4 +0,0 @@
symlink=true
node-linker=isolated
hoist-workspace-packages=false # the default in pnpm v9 is true, but that can break our runtime dependency checks
save-prefix=''

1
.nvmrc
View File

@@ -1 +0,0 @@
v22.6.0

View File

@@ -1,16 +0,0 @@
.tmp
**/.git
**/.hg
**/.pnp.*
**/.svn
**/.yarn/**
**/build
**/dist/**
**/node_modules
**/temp
**/docs/**
tsconfig.json
packages/payload/*.js
packages/payload/*.d.ts
payload-types.ts
tsconfig.tsbuildinfo

View File

@@ -1,6 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"semi": false
}

24
.swcrc
View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"sourceMaps": "inline",
"jsc": {
"target": "esnext",
"parser": {
"syntax": "typescript",
"tsx": true,
"dts": true
},
"transform": {
"react": {
"runtime": "automatic",
"pragmaFrag": "React.Fragment",
"throwIfNamespace": true,
"development": false,
"useBuiltins": true
}
}
},
"module": {
"type": "es6"
}
}

View File

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

View File

@@ -1,8 +0,0 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"firsttris.vscode-jest-runner",
"ms-playwright.playwright"
]
}

248
.vscode/launch.json vendored
View File

@@ -1,248 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
"configurations": [
{
"command": "pnpm generate:types",
"name": "Generate Types CLI",
"request": "launch",
"type": "node-terminal",
"cwd": "${workspaceFolder}"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts _community",
"cwd": "${workspaceFolder}",
"name": "Run Dev Community",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts storage-uploadthing",
"cwd": "${workspaceFolder}",
"name": "Uploadthing",
"request": "launch",
"type": "node-terminal",
"envFile": "${workspaceFolder}/test/storage-uploadthing/.env"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts live-preview",
"cwd": "${workspaceFolder}",
"name": "Run Dev Live Preview",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/loader/init.js",
"cwd": "${workspaceFolder}",
"name": "Run Loader",
"request": "launch",
"type": "node-terminal",
"env": {
"LOADER_TEST_FILE_PATH": "./dependency-test.js"
// "LOADER_TEST_FILE_PATH": "../fields/config.ts"
}
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts admin",
"cwd": "${workspaceFolder}",
"name": "Run Dev Admin",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts auth",
"cwd": "${workspaceFolder}",
"name": "Run Dev Auth",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts fields-relationship",
"cwd": "${workspaceFolder}",
"name": "Run Dev Fields-Relationship",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts login-with-username",
"cwd": "${workspaceFolder}",
"name": "Run Dev Login-With-Username",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev plugin-cloud-storage",
"cwd": "${workspaceFolder}",
"name": "Run Dev - plugin-cloud-storage",
"request": "launch",
"type": "node-terminal",
"env": {
"PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER": "s3"
}
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts collections-graphql",
"cwd": "${workspaceFolder}",
"name": "Run Dev GraphQL",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts fields",
"cwd": "${workspaceFolder}",
"name": "Run Dev Fields",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts versions",
"cwd": "${workspaceFolder}",
"name": "Run Dev Postgres",
"request": "launch",
"type": "node-terminal",
"env": {
"PAYLOAD_DATABASE": "postgres"
}
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts versions",
"cwd": "${workspaceFolder}",
"name": "Run Dev Versions",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts localization",
"cwd": "${workspaceFolder}",
"name": "Run Dev Localization",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts locked-documents",
"cwd": "${workspaceFolder}",
"name": "Run Dev Locked Documents",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts uploads",
"cwd": "${workspaceFolder}",
"name": "Run Dev Uploads",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm tsx --no-deprecation test/dev.ts field-error-states",
"cwd": "${workspaceFolder}",
"name": "Run Dev Field Error States",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run test:int live-preview",
"cwd": "${workspaceFolder}",
"name": "Live Preview Int Tests",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run test:int plugin-search",
"cwd": "${workspaceFolder}",
"name": "Search Plugin Int Tests",
"request": "launch",
"type": "node-terminal"
},
{
"command": "ts-node ./packages/payload/src/bin/index.ts build",
"env": {
"PAYLOAD_CONFIG_PATH": "test/fields/config.ts",
"PAYLOAD_BUNDLER": "vite",
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
},
"name": "Build CLI - Vite",
"outputCapture": "std",
"request": "launch",
"type": "node-terminal"
},
{
"command": "ts-node ./packages/payload/src/bin/index.ts build",
"env": {
"PAYLOAD_CONFIG_PATH": "test/fields/config.ts",
"PAYLOAD_ANALYZE_BUNDLE": "true",
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
},
"name": "Build CLI - Webpack",
"outputCapture": "std",
"request": "launch",
"type": "node-terminal"
},
{
"command": "ts-node ./packages/payload/src/bin/index.ts migrate:status",
"env": {
"PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts",
"PAYLOAD_DATABASE": "postgres",
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
// "PAYLOAD_DROP_DATABASE": "true",
},
"name": "Migrate CLI - status",
"outputCapture": "std",
"request": "launch",
"type": "node-terminal"
},
{
"command": "ts-node ./packages/payload/src/bin/index.ts migrate:create yass",
"env": {
"PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts",
"PAYLOAD_DATABASE": "postgres",
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
// "PAYLOAD_DROP_DATABASE": "true",
},
"name": "Migrate CLI - create",
"outputCapture": "std",
"request": "launch",
"type": "node-terminal"
},
{
"command": "ts-node ./packages/payload/src/bin/index.ts migrate:down",
"env": {
"PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts",
"PAYLOAD_DATABASE": "mongoose",
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
// "PAYLOAD_DROP_DATABASE": "true",
},
"name": "Migrate CLI - down",
"outputCapture": "std",
"request": "launch",
"type": "node-terminal"
},
{
"command": "ts-node ./packages/payload/src/bin/index.ts migrate:reset",
"env": {
"PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts",
"PAYLOAD_DATABASE": "mongoose",
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
// "PAYLOAD_DROP_DATABASE": "true",
},
"name": "Migrate CLI - reset",
"outputCapture": "std",
"request": "launch",
"type": "node-terminal"
},
{
"command": "ts-node ./packages/payload/src/bin/index.ts migrate:refresh",
"env": {
"PAYLOAD_CONFIG_PATH": "test/migrations-cli/config.ts",
"PAYLOAD_DATABASE": "mongoose",
"DISABLE_SWC": "true" // SWC messes up debugging the bin scripts
// "PAYLOAD_DROP_DATABASE": "true",
},
"name": "Migrate CLI - refresh",
"outputCapture": "std",
"request": "launch",
"type": "node-terminal"
}
],
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0"
}

56
.vscode/settings.json vendored
View File

@@ -1,56 +0,0 @@
{
"npm.packageManager": "pnpm",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"editor.formatOnSaveMode": "file",
"eslint.rules.customizations": [
// Defaultt all ESLint errors to 'warn' to differentate from TypeScript's 'error' level
{ "rule": "*", "severity": "warn" },
// Silence some warnings that will get auto-fixed
{ "rule": "perfectionist/*", "severity": "off", "fixable": true },
{ "rule": "curly", "severity": "off", "fixable": true },
{ "rule": "object-shorthand", "severity": "off", "fixable": true }
],
"typescript.tsdk": "node_modules/typescript/lib",
// Load .git-blame-ignore-revs file
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
"[javascript][typescript][typescriptreact]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"files.insertFinalNewline": true,
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--no-deprecation\" node 'node_modules/jest/bin/jest.js'",
"jestrunner.debugOptions": {
"runtimeArgs": ["--no-deprecation"]
}
}

View File

@@ -1,140 +0,0 @@
# Contributing to Payload
Below you'll find a set of guidelines for how to contribute to Payload.
## Opening issues
Before you submit an issue, please check all existing [open and closed issues](https://github.com/payloadcms/payload/issues) to see if your issue has previously been resolved or is already known. If there is already an issue logged, feel free to upvote it by adding a :thumbsup: [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). If you would like to submit a new issue, please fill out our Issue Template to the best of your ability so we can accurately understand your report.
## Security issues & vulnerabilities
If you come across an issue related to security, or a potential attack vector within Payload or one of its dependencies, please DO NOT create a publicly viewable issue. Instead, please contact us directly at [`dev@payloadcms.com`](mailto:dev@payloadcms.com). We will do everything we can to respond to the issue as soon as possible.
If you find a vulnerability within the core Payload repository, and we determine that it is remediable and of significant nature, we will be happy to pay you a reward for your findings and diligence. [`Contact us`](mailto:dev@payloadcms.com) to find out more.
## Documentation edits
Payload documentation can be found directly within its codebase, and you can feel free to make changes / improvements to any of it through opening a PR. We utilize these files directly in our website and will periodically deploy documentation updates as necessary.
## Building additional features
If you're an incredibly awesome person and want to help us make Payload even better through new features or additions, we would be thrilled to work with you.
## Design Contributions
When it comes to design-related changes or additions, it's crucial for us to ensure a cohesive user experience and alignment with our broader design vision. Before embarking on any implementation that would affect the design or UI/UX, we ask that you **first share your design proposal** with us for review and approval.
Our design review ensures that proposed changes fit seamlessly with other components, both existing and planned. This step is meant to prevent unintentional design inconsistencies and to save you from investing time in implementing features that might need significant design alterations later.
### Before Starting
To help us work on new features, you can create a new feature request post in [GitHub Discussion](https://github.com/payloadcms/payload/discussions) or discuss it in our [Discord](https://discord.com/invite/payload). New functionality often has large implications across the entire Payload repo, so it is best to discuss the architecture and approach before starting work on a pull request.
### Installation & Requirements
Payload is structured as a Monorepo, encompassing not only the core Payload platform but also various plugins and packages. To install all required dependencies, you have to run `pnpm install` once in the root directory. **PNPM IS REQUIRED!** Yarn or npm will not work - you will have to use pnpm to develop in the core repository. In most systems, the easiest way to install pnpm is to run `corepack enable` in your terminal.
If you're coming from a very outdated version of payload, it is recommended to nuke the node_modules folder before running pnpm install. On UNIX systems, you can easily do that using the `pnpm clean:unix` command, which will delete all node_modules folders and build artefacts.
It is also recommended to use at least Node v18 or higher. You can check your current node version by typing `node --version` in your terminal. The easiest way to switch between different node versions is to use [nvm](https://github.com/nvm-sh/nvm#intro).
### Code
Most new functionality should keep testing in mind. All top-level directories within the `test/` directory are for testing a specific category: `fields`, `collections`, etc.
If it makes sense to add your feature to an existing test directory, please do so.
A typical directory with `test/` will be structured like this:
```text
.
├── config.ts
├── int.spec.ts
├── e2e.spec.ts
└── payload-types.ts
```
- `config.ts` - This is the _granular_ Payload config for testing. It should be as lightweight as possible. Reference existing configs for an example
- `int.spec.ts` - This is the test file run by jest. Any test file must have a `*int.spec.ts` suffix.
- `e2e.spec.ts` - This is the end-to-end test file that will load up the admin UI using the above config and run Playwright tests. These tests are typically only needed if a large change is being made to the Admin UI.
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `pnpm dev:generate-types my-test-dir`. Replace `my-test-dir` with the name of your testing directory.
Each test directory is split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config.
The following command will start Payload with your config: `pnpm dev my-test-dir`. Example: `pnpm dev fields` for the test/`fields` test suite. This command will start up Payload using your config and refresh a test database on every restart. If you're using VS Code, the most common run configs are automatically added to your editor - you should be able to find them in your VS Code launch tab.
By default, payload will [automatically log you in](https://payloadcms.com/docs/authentication/overview#admin-autologin) with the default credentials. To disable that, you can either pass in the --no-auto-login flag (example: `pnpm dev my-test-dir --no-auto-login`) or set the `PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN` environment variable to `false`.
The default credentials are `dev@payloadcms.com` as E-Mail and `test` as password. These are used in the auto-login.
### Testing with your own MongoDB database
If you wish to use your own MongoDB database for the `test` directory instead of using the in memory database, all you need to do is add the following env vars to the `test/dev.ts` file:
- `process.env.NODE_ENV`
- `process.env.PAYLOAD_TEST_MONGO_URL`
- Simply set `process.env.NODE_ENV` to `test` and set `process.env.PAYLOAD_TEST_MONGO_URL` to your MongoDB URL e.g. `mongodb://127.0.0.1/your-test-db`.
### Using Postgres
If you have postgres installed on your system, you can also run the test suites using postgres. By default, mongodb is used.
To do that, simply set the `PAYLOAD_DATABASE` environment variable to `postgres`.
### Running the e2e and int tests
You can run the entire test suite using `pnpm test`. If you wish to only run e2e tests, you can use `pnpm test:e2e`. If you wish to only run int tests, you can use `pnpm test:int`.
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
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:
- `feat: adds new feature`
- `fix: fixes bug`
- `docs: adds documentation`
- `chore: does chore`
Here's a breakdown of the format. At the top-level, we use the following types to categorize our commits:
- `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.
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`
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/)
## Internationalization (i18n)
If your PR adds a string to the UI, we need to make sure to translate it into all the languages that Payload supports. To do that:
- Find the appropriate internationalization file for your package. These are typically located in `packages/translations/src/languages`, although some packages (e.g., richtext-lexical) have separate i18n files for each feature.
- Add the string to the English locale "en".
- Translate it to other languages. You can use the `translateNewKeys` script if you have an OpenAI API key in your `.env` (under `OPENAI_KEY`), or you can use ChatGPT or Google translate - whatever is easier for you. For payload core translations (in packages/translations) you can run the `translateNewKeys` script using `cd packages/translations && pnpm translateNewKeys`. For lexical translations, you can run it using `cd packages/richtext-lexical && pnpm translateNewKeys`. External contributors can skip this step and leave it to us.
To display translation strings in the UI, make sure to use the `t` utility of the `useTranslation` hook:
```ts
const { t } = useTranslation()
// ...
t('yourStringKey')
```

View File

@@ -1,69 +0,0 @@
# Reporting an issue
To report an issue, please follow the steps below:
1. Fork this repository
2. Add necessary collections/globals/fields to the `test/_community` directory to recreate the issue you are experiencing
3. Create an issue and add a link to your forked repo
**The goal is to isolate the problem by reducing the number of fields/collections you add to the test/\_community folder. This folder is not meant for you to copy your project into, but to recreate the issue you are experiencing with minimal config.**
## Test directory file tree explanation
```text
.
├── config.ts
├── int.spec.ts
├── e2e.spec.ts
└── payload-types.ts
```
- `config.ts` - This is the _granular_ Payload config for testing. It should be as lightweight as possible. Reference existing configs for an example
- `int.spec.ts` [Optional] - This is the test file run by jest. Any test file must have a `*int.spec.ts` suffix.
- `e2e.spec.ts` [Optional] - This is the end-to-end test file that will load up the admin UI using the above config and run Playwright tests.
- `payload-types.ts` - Generated types from `config.ts`. Generate this file by running `pnpm dev:generate-types _community`.
The directory split up in this way specifically to reduce friction when creating tests and to add the ability to boot up Payload with that specific config. You should modify the files in `test/_community` to get started.
## How to start test collection admin UI
To start the admin panel so you can manually recreate your issue, you can run the following command:
```bash
# This command will start up Payload using your config
# NOTE: it will wipe the test database on restart
pnpm dev _community
```
## Testing is optional but encouraged
An issue does not need to have failing tests — reproduction steps with your forked repo are enough at this point. Some people like to dive deeper and we want to give you the guidance/tools to do so. Read more below.
### How to run integration tests (Payload API tests)
There are a couple ways to do this:
- **Granularly** - you can run individual tests in vscode by installing the Jest Runner plugin and using that to run individual tests. Clicking the `debug` button will run the test in debug mode allowing you to set break points.
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/assets/images/github/int-debug.png" />
- **Manually** - you can run all int tests in the `/test/_community/int.spec.ts` file by running the following command:
```bash
pnpm test:int _community
```
### How to run E2E tests (Admin Panel UI tests)
The easiest way to run E2E tests is to install
- [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright)
- [Playwright Runner](https://marketplace.visualstudio.com/items?itemName=ortoni.ortoni)
Once they are installed you can open the `testing` tab in vscode sidebar and drill down to the test you want to run, i.e. `/test/_community/e2e.spec.ts`
<img src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/payload/src/assets/images/github/e2e-debug.png" />
#### Notes
- It is recommended to add the test credentials (located in `test/credentials.ts`) to your autofill for `localhost:3000/admin` as this will be required on every nodemon restart. The default credentials are `dev@payloadcms.com` as email and `test` as password.

View File

@@ -1,22 +0,0 @@
MIT License
Copyright (c) 2018-2025 Payload CMS, Inc. <info@payloadcms.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

133
README.md
View File

@@ -1,133 +0,0 @@
<a href="https://payloadcms.com"><img width="100%" src="https://l4wlsi8vxy8hre4v.public.blob.vercel-storage.com/github-banner-new-logo.jpg" alt="Payload headless CMS Admin panel built with React" /></a>
<br />
<br />
<p align="left">
<a href="https://github.com/payloadcms/payload/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/payloadcms/payload/main.yml?style=flat-square"></a>
&nbsp;
<a href="https://discord.gg/payload"><img alt="Discord" src="https://img.shields.io/discord/967097582721572934?label=Discord&color=7289da&style=flat-square" /></a>
&nbsp;
<a href="https://www.npmjs.com/package/payload"><img alt="npm" src="https://img.shields.io/npm/dw/payload?style=flat-square" /></a>
&nbsp;
<a href="https://github.com/payloadcms/payload/graphs/contributors"><img alt="npm" src="https://img.shields.io/github/contributors-anon/payloadcms/payload?color=yellow&style=flat-square" /></a>
&nbsp;
<a href="https://www.npmjs.com/package/payload"><img alt="npm" src="https://img.shields.io/npm/v/payload?style=flat-square" /></a>
&nbsp;
<a href="https://twitter.com/payloadcms"><img src="https://img.shields.io/badge/follow-payloadcms-1DA1F2?logo=twitter&style=flat-square" alt="Payload Twitter" /></a>
</p>
<hr/>
<h4>
<a target="_blank" href="https://payloadcms.com/docs/getting-started/what-is-payload" rel="dofollow"><strong>Explore the Docs</strong></a>&nbsp;·&nbsp;<a target="_blank" href="https://payloadcms.com/community-help" rel="dofollow"><strong>Community Help</strong></a>&nbsp;·&nbsp;<a target="_blank" href="https://github.com/payloadcms/payload/discussions/1539" rel="dofollow"><strong>Roadmap</strong></a>&nbsp;·&nbsp;<a target="_blank" href="https://www.g2.com/products/payload-cms/reviews#reviews" rel="dofollow"><strong>View G2 Reviews</strong></a>
</h4>
<hr/>
> [!IMPORTANT]
> 🎉 <strong>We've released 3.0!</strong> Star this repo or keep an eye on it to follow along.
Payload is the first-ever Next.js native CMS that can install directly in your existing `/app` folder. It's the start of a new era for headless CMS.
<h3>Benefits over a regular CMS</h3>
<ul>
<li>Deploy anywhere, including serverless on Vercel for free</li>
<li>Combine your front+backend in the same <code>/app</code> folder if you want</li>
<li>Don't sign up for yet another SaaS - Payload is open source</li>
<li>Query your database in React Server Components</li>
<li>Both admin and backend are 100% extensible</li>
<li>No vendor lock-in</li>
<li>Never touch ancient WP code again</li>
<li>Build faster, never hit a roadblock</li>
</ul>
## Quickstart
Before beginning to work with Payload, make sure you have all of the [required software](https://payloadcms.com/docs/getting-started/installation).
```text
pnpx create-payload-app@latest
```
**If you're new to Payload, you should start with the website template** (`pnpx create-payload-app@latest -t website`). It shows how to do _everything_ - including custom Rich Text blocks, on-demand revalidation, live preview, and more. It comes with a frontend built with Tailwind all in one `/app` folder.
## One-click templates
Jumpstart your next project by starting with a pre-made template. These are production-ready, end-to-end solutions designed to get you to market as fast as possible.
### [🌐 Website](https://github.com/payloadcms/payload/tree/main/templates/website)
Build any kind of website, blog, or portfolio from small to enterprise. Comes with a fully functional front-end built with RSCs and Tailwind.
We're constantly adding more templates to our [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates). If you maintain your own template, consider adding the `payload-template` topic to your GitHub repository for others to find.
- [Official Templates](https://github.com/payloadcms/payload/tree/main/templates)
- [Community Templates](https://github.com/topics/payload-template)
## ✨ Features
- Completely free and open-source
- Next.js native, built to run inside _your_ `/app` folder
- Use server components to extend Payload UI
- Query your database directly in server components, no need for REST / GraphQL
- Fully TypeScript with automatic types for your data
- [Auth out of the box](https://payloadcms.com/docs/authentication/overview)
- [Versions and drafts](https://payloadcms.com/docs/versions/overview)
- [Localization](https://payloadcms.com/docs/configuration/localization)
- [Block-based layout builder](https://payloadcms.com/docs/fields/blocks)
- [Customizable React admin](https://payloadcms.com/docs/admin/overview)
- [Lexical rich text editor](https://payloadcms.com/docs/fields/rich-text)
- [Conditional field logic](https://payloadcms.com/docs/fields/overview#conditional-logic)
- Extremely granular [Access Control](https://payloadcms.com/docs/access-control/overview)
- [Document and field-level hooks](https://payloadcms.com/docs/hooks/overview) for every action Payload provides
- Intensely fast API
- Highly secure thanks to HTTP-only cookies, CSRF protection, and more
<a target="_blank" href="https://github.com/payloadcms/payload/discussions"><strong>Request Feature</strong></a>
## 🗒️ Documentation
Check out the [Payload website](https://payloadcms.com/docs/getting-started/what-is-payload) to find in-depth documentation for everything that Payload offers.
Migrating from v2 to v3? Check out the [3.0 Migration Guide](https://github.com/payloadcms/payload/blob/main/docs/migration-guide/overview.mdx) on how to do it.
## 🙋 Contributing
If you want to add contributions to this repository, please follow the instructions in [contributing.md](./CONTRIBUTING.md).
## 📚 Examples
The [Examples Directory](./examples) is a great resource for learning how to setup Payload in a variety of different ways, but you can also find great examples in our blog and throughout our social media.
If you'd like to run the examples, you can use `create-payload-app` to create a project from one:
```sh
npx create-payload-app --example example_name
```
You can see more examples at:
- [Examples Directory](./examples)
- [Payload Blog](https://payloadcms.com/blog)
- [Payload YouTube](https://www.youtube.com/@payloadcms)
## 🔌 Plugins
Payload is highly extensible and allows you to install or distribute plugins that add or remove functionality. There are both officially-supported and community-supported plugins available. If you maintain your own plugin, consider adding the `payload-plugin` topic to your GitHub repository for others to find.
- [Official Plugins](https://github.com/orgs/payloadcms/repositories?q=topic%3Apayload-plugin)
- [Community Plugins](https://github.com/topics/payload-plugin)
## 🚨 Need help?
There are lots of good conversations and resources in our Github Discussions board and our Discord Server. If you're struggling with something, chances are, someone's already solved what you're up against. :point_down:
- [GitHub Discussions](https://github.com/payloadcms/payload/discussions)
- [GitHub Issues](https://github.com/payloadcms/payload/issues)
- [Discord](https://t.co/30APlsQUPB)
- [Community Help](https://payloadcms.com/community-help)
## ⭐ Like what we're doing? Give us a star
![payload-github-star](https://cms.payloadcms.com/media/payload-github-star.gif)
## 👏 Thanks to all our contributors
<img align="left" src="https://contributors-img.web.app/image?repo=payloadcms/payload"/>

View File

@@ -1,5 +0,0 @@
# Security Policy
## Reporting a Vulnerability
Please report any security issues or concerns to [info@payloadcms.com](mailto:info@payloadcms.com).

View File

@@ -1,14 +0,0 @@
import React from 'react'
export const metadata = {
description: 'Generated by Next.js',
title: 'Next.js',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

View File

@@ -1,11 +0,0 @@
import configPromise from '@payload-config'
import { getPayloadHMR } from '@payloadcms/next/utilities'
export const Page = async ({ params, searchParams }) => {
const payload = await getPayloadHMR({
config: configPromise,
})
return <div>test ${payload?.config?.collections?.length}</div>
}
export default Page

View File

@@ -1,25 +0,0 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, importMap, params, searchParams })
export default NotFound

View File

@@ -1,25 +0,0 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'
import config from '@payload-config'
import { generatePageMetadata, RootPage } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = {
params: Promise<{
segments: string[]
}>
searchParams: Promise<{
[key: string]: string | string[]
}>
}
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, importMap, params, searchParams })
export default Page

View File

@@ -1,10 +0,0 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
export const GET = REST_GET(config)
export const POST = REST_POST(config)
export const DELETE = REST_DELETE(config)
export const PATCH = REST_PATCH(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -1,6 +0,0 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
export const GET = GRAPHQL_PLAYGROUND_GET(config)

View File

@@ -1,8 +0,0 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@payload-config'
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
export const POST = GRAPHQL_POST(config)
export const OPTIONS = REST_OPTIONS(config)

View File

@@ -1,8 +0,0 @@
#custom-css {
font-family: monospace;
background-image: url('/placeholder.png');
}
#custom-css::after {
content: 'custom-css';
}

View File

@@ -1,32 +0,0 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
// import '@payloadcms/ui/styles.css' // Uncomment this line if `@payloadcms/ui` in `tsconfig.json` points to `/ui/dist` instead of `/ui/src`
import type { ServerFunctionClient } from 'payload'
import config from '@payload-config'
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
import React from 'react'
import { importMap } from './admin/importMap.js'
import './custom.scss'
type Args = {
children: React.ReactNode
}
const serverFunction: ServerFunctionClient = async function (args) {
'use server'
return handleServerFunctions({
...args,
config,
importMap,
})
}
const Layout = ({ children }: Args) => (
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
{children}
</RootLayout>
)
export default Layout

View File

@@ -1,27 +0,0 @@
/* eslint-disable no-restricted-exports */
'use client'
import * as Sentry from '@sentry/nextjs'
import NextError from 'next/error.js'
import { useEffect } from 'react'
export default function GlobalError({ error }: { error: { digest?: string } & Error }) {
useEffect(() => {
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
Sentry.captureException(error)
}
}, [error])
return (
<html lang="en-US">
<body>
{/* `NextError` is the default Next.js error page component. Its type
definition requires a `statusCode` prop. However, since the App Router
does not expose status codes for errors, we simply pass 0 to render a
generic error message. */}
{/* @ts-expect-error types repo */}
<NextError statusCode={0} />
</body>
</html>
)
}

View File

@@ -1,5 +0,0 @@
export const GET = () => {
return Response.json({
hello: 'elliot',
})
}

View File

@@ -1,339 +0,0 @@
---
title: Collection Access Control
label: Collections
order: 20
desc: With Collection-level Access Control you can define which users can create, read, update or delete Collections.
keywords: collections, access control, permissions, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Collection Access Control is [Access Control](../access-control/overview) used to restrict access to Documents within a [Collection](../getting-started/concepts#collections), as well as what they can and cannot see within the [Admin Panel](../admin/overview) as it relates to that Collection.
To add Access Control to a Collection, use the `access` property in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload';
export const CollectionWithAccessControl: CollectionConfig = {
// ...
access: { // highlight-line
// ...
},
}
```
## Config Options
Access Control is specific to the operation of the request.
To add Access Control to a Collection, use the `access` property in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload';
export const CollectionWithAccessControl: CollectionConfig = {
// ...
// highlight-start
access: {
create: () => {...},
read: () => {...},
update: () => {...},
delete: () => {...},
// Auth-enabled Collections only
admin: () => {...},
unlock: () => {...},
// Version-enabled Collections only
readVersions: () => {...},
},
// highlight-end
}
```
The following options are available:
| Function | Allows/Denies Access |
| ----------------------- | -------------------------------------------- |
| **`create`** | Used in the `create` operation. [More details](#create). |
| **`read`** | Used in the `find` and `findByID` operations. [More details](#read). |
| **`update`** | Used in the `update` operation. [More details](#update). |
| **`delete`** | Used in the `delete` operation. [More details](#delete). |
If a Collection supports [`Authentication`](../authentication/overview), the following additional options are available:
| Function | Allows/Denies Access |
| ----------------------- | -------------------------------------------------------------- |
| **`admin`** | Used to restrict access to the [Admin Panel](../admin/overview). [More details](#admin). |
| **`unlock`** | Used to restrict which users can access the `unlock` operation. [More details](#unlock). |
If a Collection supports [Versions](../versions/overview), the following additional options are available:
| Function | Allows/Denies Access |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| **`readVersions`** | Used to control who can read versions, and who can't. Will automatically restrict the Admin UI version viewing access. [More details](#read-versions). |
### Create
Returns a boolean which allows/denies access to the `create` request.
To add create Access Control to a Collection, use the `create` property in the [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const CollectionWithCreateAccess: CollectionConfig = {
// ...
access: {
// highlight-start
create: ({ req: { user }, data }) => {
return Boolean(user)
},
// highlight-end
},
}
```
The following arguments are provided to the `create` function:
| Option | Description |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------- |
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
| **`data`** | The data passed to create the document with. |
### Read
Returns a boolean which allows/denies access to the `read` request.
To add read Access Control to a Collection, use the `read` property in the [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const CollectionWithReadAccess: CollectionConfig = {
// ...
access: {
// highlight-start
read: ({ req: { user } }) => {
return Boolean(user)
},
// highlight-end
},
}
```
<Banner type="success">
**Tip:**
Return a [Query](../queries/overview) to limit the Documents to only those that match the constraint. This can be helpful to restrict users' access to specific Documents. [More details](../queries/overview).
</Banner>
As your application becomes more complex, you may want to define your function in a separate file and import them into your Collection Config:
```ts
import type { Access } from 'payload'
export const canReadPage: Access = ({ req: { user } }) => {
// Allow authenticated users
if (user) {
return true
}
// By returning a Query, guest users can read public Documents
// Note: this assumes you have a `isPublic` checkbox field on your Collection
return {
isPublic: {
equals: true,
},
}
}
```
The following arguments are provided to the `read` function:
| Option | Description |
| --------- | -------------------------------------------------------------------------- |
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
| **`id`** | `id` of document requested, if within `findByID`. |
### Update
Returns a boolean which allows/denies access to the `update` request.
To add update Access Control to a Collection, use the `update` property in the [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const CollectionWithUpdateAccess: CollectionConfig = {
// ...
access: {
// highlight-start
update: ({ req: { user }}) => {
return Boolean(user)
},
// highlight-end
},
}
```
<Banner type="success">
**Tip:**
Return a [Query](../queries/overview) to limit the Documents to only those that match the constraint. This can be helpful to restrict users' access to specific Documents. [More details](../queries/overview).
</Banner>
As your application becomes more complex, you may want to define your function in a separate file and import them into your Collection Config:
```ts
import type { Access } from 'payload'
export const canUpdateUser: Access = ({ req: { user }, id }) => {
// Allow users with a role of 'admin'
if (user.roles && user.roles.some((role) => role === 'admin')) {
return true
}
// allow any other users to update only oneself
return user.id === id
}
```
The following arguments are provided to the `update` function:
| Option | Description |
| ---------- | -------------------------------------------------------------------------- |
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
| **`id`** | `id` of document requested to update. |
| **`data`** | The data passed to update the document with. |
### Delete
Similarly to the Update function, returns a boolean or a [query constraint](/docs/queries/overview) to limit which documents can be deleted by which users.
To add delete Access Control to a Collection, use the `delete` property in the [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const CollectionWithDeleteAccess: CollectionConfig = {
// ...
access: {
// highlight-start
delete: ({ req: { user }}) => {
return Boolean(user)
},
// highlight-end
},
}
```
As your application becomes more complex, you may want to define your function in a separate file and import them into your Collection Config:
```ts
import type { Access } from 'payload'
export const canDeleteCustomer: Access = async ({ req, id }) => {
if (!id) {
// allow the admin UI to show controls to delete since it is indeterminate without the `id`
return true
}
// Query another Collection using the `id`
const result = await req.payload.find({
collection: 'contracts',
limit: 0,
depth: 0,
where: {
customer: { equals: id },
},
})
return result.totalDocs === 0
}
```
The following arguments are provided to the `delete` function:
| Option | Description |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object with additional `user` property, which is the currently logged in user. |
| **`id`** | `id` of document requested to delete. |
### Admin
If the Collection is used to access the [Admin Panel](../admin/overview#the-admin-user-collection), the `Admin` Access Control function determines whether or not the currently logged in user can access the admin UI.
To add Admin Access Control to a Collection, use the `admin` property in the [Collection Config](../collections/overview):
```ts
import type { CollectionConfig } from 'payload'
export const CollectionWithAdminAccess: CollectionConfig = {
// ...
access: {
// highlight-start
admin: ({ req: { user }}) => {
return Boolean(user)
},
// highlight-end
},
}
```
The following arguments are provided to the `admin` function:
| Option | Description |
| --------- | -------------------------------------------------------------------------- |
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
### Unlock
Determines which users can [unlock](/docs/authentication/operations#unlock) other users who may be blocked from authenticating successfully due to [failing too many login attempts](/docs/authentication/overview#config-options).
To add Unlock Access Control to a Collection, use the `unlock` property in the [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const CollectionWithUnlockAccess: CollectionConfig = {
// ...
access: {
// highlight-start
unlock: ({ req: { user }}) => {
return Boolean(user)
},
// highlight-end
},
}
```
The following arguments are provided to the `unlock` function:
| Option | Description |
| --------- | -------------------------------------------------------------------------- |
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
### Read Versions
If the Collection has [Versions](../versions/overview) enabled, the `readVersions` Access Control function determines whether or not the currently logged in user can access the version history of a Document.
To add Read Versions Access Control to a Collection, use the `readVersions` property in the [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const CollectionWithVersionsAccess: CollectionConfig = {
// ...
access: {
// highlight-start
readVersions: ({ req: { user }}) => {
return Boolean(user)
},
// highlight-end
},
}
```
The following arguments are provided to the `readVersions` function:
| Option | Description |
| --------- | -------------------------------------------------------------------------- |
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |

View File

@@ -1,103 +0,0 @@
---
title: Field-level Access Control
label: Fields
order: 40
desc: Field-level Access Control is specified within a field's config, and allows you to define which users can create, read or update Fields.
keywords: fields, access control, permissions, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Field Access Control is [Access Control](../access-control/overview) used to restrict access to specific [Fields](../fields/overview) within a Document.
To add Access Control to a Field, use the `access` property in your [Field Config](../fields/overview):
```ts
import type { Field } from 'payload';
export const FieldWithAccessControl: Field = {
// ...
access: { // highlight-line
// ...
},
}
```
<Banner type="warning">
**Note:**
Field Access Controls does not support returning [Query](../queries/overview) constraints like [Collection Access Control](./collections) does.
</Banner>
## Config Options
Access Control is specific to the operation of the request.
To add Access Control to a Field, use the `access` property in the [Field Config](../fields/overview):
```ts
import type { CollectionConfig } from 'payload';
export const Posts: CollectionConfig = {
slug: 'posts',
fields: [
{
name: 'title',
type: 'text',
// highlight-start
access: {
create: ({ req: { user } }) => { ... },
read: ({ req: { user } }) => { ... },
update: ({ req: { user } }) => { ... },
},
// highlight-end
};
],
};
```
The following options are available:
| Function | Purpose |
| ----------------------- | -------------------------------------------------------------------------------- |
| **`create`** | Allows or denies the ability to set a field's value when creating a new document. [More details](#create). |
| **`read`** | Allows or denies the ability to read a field's value. [More details](#read). |
| **`update`** | Allows or denies the ability to update a field's value [More details](#update). |
### Create
Returns a boolean which allows or denies the ability to set a field's value when creating a new document. If `false` is returned, any passed values will be discarded.
**Available argument properties:**
| Option | Description |
| ----------------- | -------------------------------------------------------------------------- |
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` |
| **`data`** | The full data passed to create the document. |
| **`siblingData`** | Immediately adjacent field data passed to create the document. |
### Read
Returns a boolean which allows or denies the ability to read a field's value. If `false`, the entire property is omitted from the resulting document.
**Available argument properties:**
| Option | Description |
| ----------------- | -------------------------------------------------------------------------- |
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` |
| **`id`** | `id` of the document being read |
| **`doc`** | The full document data. |
| **`siblingData`** | Immediately adjacent field data of the document being read. |
### Update
Returns a boolean which allows or denies the ability to update a field's value. If `false` is returned, any passed values will be discarded.
If `false` is returned and you attempt to update the field's value, the operation will **not** throw an error however the field will be omitted from the update operation and the value will remain unchanged.
**Available argument properties:**
| Option | Description |
| ----------------- | -------------------------------------------------------------------------- |
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` |
| **`id`** | `id` of the document being updated |
| **`data`** | The full data passed to update the document. |
| **`siblingData`** | Immediately adjacent field data passed to update the document with. |
| **`doc`** | The full document data, before the update is applied. |

View File

@@ -1,142 +0,0 @@
---
title: Globals Access Control
label: Globals
order: 30
desc: Global-level Access Control is specified within each Global's `access` property and allows you to define which users can read or update Globals.
keywords: globals, access control, permissions, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Global Access Control is [Access Control](../access-control/overview) used to restrict access to [Global](../configuration/globals) Documents, as well as what they can and cannot see within the [Admin Panel](../admin/overview) as it relates to that Global.
To add Access Control to a Global, use the `access` property in your [Global Config](../configuration/globals):
```ts
import type { GlobalConfig } from 'payload';
export const GlobalWithAccessControl: GlobalConfig = {
// ...
access: { // highlight-line
// ...
},
}
```
## Config Options
Access Control is specific to the operation of the request.
To add Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../configuration/globals):
```ts
import { GlobalConfig } from 'payload'
const GlobalWithAccessControl: GlobalConfig = {
// ...
// highlight-start
access: {
read: ({ req: { user } }) => {...},
update: ({ req: { user } }) => {...},
// Version-enabled Globals only
readVersion: () => {...},
},
// highlight-end
}
export default Header
```
The following options are available:
| Function | Allows/Denies Access |
| ----------------------- | -------------------------------------- |
| **`read`** | Used in the `findOne` Global operation. [More details](#read). |
| **`update`** | Used in the `update` Global operation. [More details](#update). |
If a Global supports [Versions](../versions/overview), the following additional options are available:
| Function | Allows/Denies Access |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| **`readVersions`** | Used to control who can read versions, and who can't. Will automatically restrict the Admin UI version viewing access. [More details](#read-versions). |
### Read
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):
```ts
import { GlobalConfig } from 'payload'
const Header: GlobalConfig = {
// ...
// highlight-start
read: {
read: ({ req: { user } }) => {
return Boolean(user)
},
}
// highlight-end
}
```
The following arguments are provided to the `read` function:
| Option | Description |
| --------- | -------------------------------------------------------------------------- |
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
### Update
Returns a boolean result or optionally a [query constraint](../queries/overview) which limits who can update this global based on its current properties.
To add update Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../configuration/globals):
```ts
import { GlobalConfig } from 'payload'
const Header: GlobalConfig = {
// ...
// highlight-start
access: {
update: ({ req: { user }, data }) => {
return Boolean(user)
},
}
// highlight-end
}
```
The following arguments are provided to the `update` function:
| Option | Description |
| ---------- | -------------------------------------------------------------------------- |
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |
| **`data`** | The data passed to update the global with. |
### Read Versions
If the Global has [Versions](../versions/overview) enabled, the `readVersions` Access Control function determines whether or not the currently logged in user can access the version history of a Document.
To add Read Versions Access Control to a Collection, use the `readVersions` property in the [Global Config](../configuration/globals):
```ts
import type { GlobalConfig } from 'payload'
export const GlobalWithVersionsAccess: GlobalConfig = {
// ...
access: {
// highlight-start
readVersions: ({ req: { user }}) => {
return Boolean(user)
},
// highlight-end
},
}
```
The following arguments are provided to the `readVersions` function:
| Option | Description |
| --------- | -------------------------------------------------------------------------- |
| **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. |

View File

@@ -1,79 +0,0 @@
---
title: Access Control
label: Overview
order: 10
desc: Payload makes it simple to define and manage Access Control. By declaring roles, you can set permissions and restrict what your users can interact with.
keywords: overview, access control, permissions, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
<YouTube id="DoPLyXG26Dg" title="Overview of Payload Access Control" />
Access Control determines what a user can and cannot do with any given Document, as well as what they can and cannot see within the [Admin Panel](../admin/overview). By implementing Access Control, you can define granular restrictions based on the user, their roles (RBAC), Document data, or any other criteria your application requires.
Access Control functions are scoped to the _operation_, meaning you can have different rules for `create`, `read`, `update`, `delete`, etc. Access Control functions are executed _before_ any changes are made and _before_ any operations are completed. This allows you to determine if the user has the necessary permissions before fulfilling the request.
There are many use cases for Access Control, including:
- Allowing anyone `read` access to all posts
- Only allowing public access to posts where a `status` field is equal to `published`
- Giving only users with a `role` field equal to `admin` the ability to delete posts
- Allowing anyone to submit contact forms, but only logged in users to `read`, `update` or `delete` them
- Restricting a user to only be able to see their own orders, but no-one else's
- Allowing users that belong to a certain organization to access only that organization's resources
There are three main types of Access Control in Payload:
- [Collection Access Control](./collections)
- [Global Access Control](./globals)
- [Field Access Control](./fields)
## Default Access Control
Payload provides default Access Control so that your data is secured behind [Authentication](../authentication/overview) without additional configuration. To do this, Payload sets a default function that simply checks if a user is present on the request. You can override this default behavior by defining your own Access Control functions as needed.
Here is the default Access Control that Payload provides:
```ts
const defaultPayloadAccess = ({ req: { user } }) => {
// Return `true` if a user is found
// and `false` if it is undefined or null
return Boolean(user) // highlight-line
}
```
<Banner type="warning">
**Important:**
In the [Local API](../local-api/overview), all Access Control is _skipped_ by default. This allows your server to have full control over your application. To opt back in, you can set the `overrideAccess` option to `false` in your requests.
</Banner>
## The Access Operation
The Admin Panel responds dynamically to your changes to Access Control. For example, if you restrict editing `ExampleCollection` to only users that feature an "admin" role, Payload will **hide** that Collection from the Admin Panel entirely. This is super powerful and allows you to control who can do what within your Admin Panel using the same functions that secure your APIs.
To accomplish this, Payload exposes the [Access Operation](../authentication/operations#access). Upon login, Payload executes each Access Control function at the top level, across all Collections, Globals, and Fields, and returns a response that contains a reflection of what the currently authenticated user can do within your application.
<Banner type="warning">
**Important:**
When your access control functions are executed via the [Access Operation](../authentication/operations#access), the `id` and `data` arguments will be `undefined`. This is because Payload is executing your functions without referencing a specific Document.
</Banner>
If you use `id` or `data` within your access control functions, make sure to check that they are defined first. If they are not, then you can assume that your Access Control is being executed via the Access Operation to determine solely what the user can do within the Admin Panel.
## Locale Specific Access Control
To implement locale-specific access control, you can use the `req.locale` argument in your access control functions. This argument allows you to evaluate the current locale of the request and determine access permissions accordingly.
Here is an example:
```ts
const access = ({ req }) => {
// Grant access if the locale is 'en'
if (req.locale === 'en') {
return true;
}
// Deny access for all other locales
return false;
}
```

View File

@@ -1,183 +0,0 @@
---
title: Collection Admin Config
label: Collections
order: 20
desc:
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
The behavior of [Collections](../configuration/collections) within the [Admin Panel](./overview) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](./components), selecting which fields to display in the List View, and more.
To configure Admin Options for Collections, use the `admin` property in your Collection Config:
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: { // highlight-line
// ...
},
}
```
## Admin Options
The following options are available:
| Option | Description |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`group`** | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide the link from the navigation while keeping its routes accessible. |
| **`hidden`** | Set to true or a function, called with the current user, returning true to exclude this Collection from navigation and admin routing. |
| **`hooks`** | Admin-specific hooks for this Collection. [More details](../hooks/collections). |
| **`useAsTitle`** | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. A field with `virtual: true` cannot be used as the title. |
| **`description`** | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#custom-components). |
| **`defaultColumns`** | Array of field names that correspond to which columns to show by default in this Collection's List View. |
| **`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. |
| **`meta`** | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](./metadata). |
| **`preview`** | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](#preview). |
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| **`components`** | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
| **`listSearchableFields`** | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
| **`pagination`** | Set pagination-specific options for this Collection. [More details](#pagination). |
| **`baseListFilter`** | You can define a default base filter for this collection's List view, which will be merged into any filters that the user performs. |
### Custom Components
Collections can set their own [Custom Components](./components) which only apply to [Collection](../configuration/collections)-specific UI within the [Admin Panel](./overview). This includes elements such as the Save Button, or entire layouts such as the Edit View.
To override Collection Components, use the `admin.components` property in your [Collection Config](../configuration/collections):
```ts
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollection: SanitizedCollectionConfig = {
// ...
admin: {
components: { // highlight-line
// ...
},
},
}
```
The following options are available:
| Path | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **`beforeList`** | An array of components to inject _before_ the built-in List View |
| **`beforeListTable`** | An array of components to inject _before_ the built-in List View's table |
| **`afterList`** | An array of components to inject _after_ the built-in List View |
| **`afterListTable`** | An array of components to inject _after_ the built-in List View's table |
| **`Description`** | A component to render below the Collection label in the List View. An alternative to the `admin.description` property. |
| **`edit.SaveButton`** | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. |
| **`edit.SaveDraftButton`** | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. |
| **`edit.PublishButton`** | Replace the default Publish Button with a Custom Component. [Drafts](../versions/drafts) must be enabled. |
| **`edit.PreviewButton`** | Replace the default Preview Button with a Custom Component. [Preview](#preview) must be enabled. |
| **`edit.Upload`** | Replace the default Upload component with a Custom Component. [Upload](../upload/overview) must be enabled. |
| **`views`** | Override or create new views within the Admin Panel. [More details](./views). |
<Banner type="success">
**Note:**
For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components).
</Banner>
### Preview
It is possible to display a Preview Button within the Edit View of the Admin Panel. This will allow editors to visit the frontend of your app the corresponds to the document they are actively editing. This way they can preview the latest, potentially unpublished changes.
To configure the Preview Button, set the `admin.preview` property to a function in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
// ...
admin: {
// highlight-start
preview: (doc, { locale }) => {
if (doc?.slug) {
return `/${doc.slug}?locale=${locale}`
}
return null
},
// highlight-end
},
}
```
The `preview` property resolves to a string that points to your front-end application with additional URL parameters. This can be an absolute URL or a relative path.
The preview function receives two arguments:
| Argument | Description |
| --- | --- |
| **`doc`** | The Document being edited. |
| **`ctx`** | An object containing `locale`, `token`, and `req` properties. The `token` is the currently logged-in user's JWT. |
If your application requires a fully qualified URL, such as within deploying to Vercel Preview Deployments, you can use the `req` property to build this URL:
```ts
preview: (doc, { req }) => `${req.protocol}//${req.host}/${doc.slug}` // highlight-line
```
<Banner type="success">
**Note:**
For fully working example of this, check of the official [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview) in the [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples).
</Banner>
### Pagination
All Collections receive their own List View which displays a paginated list of documents that can be sorted and filtered. The pagination behavior of the List View can be customized on a per-Collection basis, and uses the same [Pagination](../queries/pagination) API that Payload provides.
To configure pagination options, use the `admin.pagination` property in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
// ...
admin: {
// highlight-start
pagination: {
defaultLimit: 10,
limits: [10, 20, 50],
},
// highlight-end
},
}
```
The following options are available:
| Option | Description |
| -------------- | --------------------------------------------------------------------------------------------------- |
| `defaultLimit` | Integer that specifies the default per-page limit that should be used. Defaults to 10. |
| `limits` | Provide an array of integers to use as per-page options for admins to choose from in the List View. |
### List Searchable Fields
In the List View, there is a "search" box that allows you to quickly find a document through a simple text search. By default, it searches on the ID field. If defined, the `admin.useAsTitle` field is used. Or, you can explicitly define which fields to search based on the needs of your application.
To define which fields should be searched, use the `admin.listSearchableFields` property in your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
// ...
admin: {
// highlight-start
listSearchableFields: ['title', 'slug'],
// highlight-end
},
}
```
<Banner type="warning">
**Tip:**
If you are adding `listSearchableFields`, make sure you index each of these fields so your admin queries can remain performant.
</Banner>

View File

@@ -1,550 +0,0 @@
---
title: Swap in your own React components
label: Custom Components
order: 40
desc: Fully customize your Admin Panel by swapping in your own React components. Add fields, remove views, update routes and change functions to sculpt your perfect Dashboard.
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
The Payload [Admin Panel](./overview) is designed to be as minimal and straightforward as possible to allow for easy customization and full control over the UI. In order for Payload to support this level of customization, Payload provides a pattern for you to supply your own React components through your [Payload Config](../configuration/overview).
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default. This enables the use of the [Local API](../local-api/overview) directly on the front-end. Custom Components are available for nearly every part of the Admin Panel for extreme granularity and control.
<Banner type="success">
**Note:**
Client Components continue to be fully supported. To use Client Components in your app, simply include the `use client` directive. Payload will automatically detect and remove all default, [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types) before rendering your component. [More details](#client-components).
</Banner>
There are four main types of Custom Components in Payload:
- [Root Components](#root-components)
- [Collection Components](./collections#custom-components)
- [Global Components](./globals#custom-components)
- [Field Components](./fields#custom-components)
To swap in your own Custom Component, first consult the list of available components, determine the scope that corresponds to what you are trying to accomplish, then [author your React component(s)](#building-custom-components) accordingly.
## Defining Custom Components
As Payload compiles the Admin Panel, it checks your config for Custom Components. When detected, Payload either replaces its own default component with yours, or if none exists by default, renders yours outright. While are many places where Custom Components are supported in Payload, each is defined in the same way using [Component Paths](#component-paths).
To add a Custom Component, point to its file path in your Payload Config:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
logout: {
Button: '/src/components/Logout#MyComponent' // highlight-line
}
}
},
})
```
<Banner type="success">
**Note:**
All Custom Components can be either Server Components or Client Components, depending on the presence of the `use client` directive at the top of the file.
</Banner>
### Component Paths
In order to ensure the Payload Config is fully Node.js compatible and as lightweight as possible, components are not directly imported into your config. Instead, they are identified by their file path for the Admin Panel to resolve on its own.
Component Paths, by default, are relative to your project's base directory. This is either your current working directory, or the directory specified in `config.admin.baseDir`. To simplify Component Paths, you can also configure the base directory using the `admin.importMap.baseDir` property.
Components using named exports are identified either by appending `#` followed by the export name, or using the `exportName` property. If the component is the default export, this can be omitted.
```ts
import { buildConfig } from 'payload'
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const config = buildConfig({
// ...
admin: {
importMap: {
baseDir: path.resolve(dirname, 'src'), // highlight-line
},
components: {
logout: {
Button: '/components/Logout#MyComponent' // highlight-line
}
}
},
})
```
In this example, we set the base directory to the `src` directory, and omit the `/src/` part of our component path string.
### Config Options
While Custom Components are usually defined as a string, you can also pass in an object with additional options:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
logout: {
// highlight-start
Button: {
path: '/src/components/Logout',
exportName: 'MyComponent',
}
// highlight-end
}
}
},
})
```
The following options are available:
| Property | Description |
|---------------|-------------------------------------------------------------------------------------------------------------------------------|
| **`clientProps`** | Props to be passed to the Custom Components if it's a Client Component. [More details](#custom-props). |
| **`exportName`** | Instead of declaring named exports using `#` in the component path, you can also omit them from `path` and pass them in here. |
| **`path`** | File path to the Custom Component. Named exports can be appended to the end of the path, separated by a `#`. |
| **`serverProps`** | Props to be passed to the Custom Component if it's a Server Component. [More details](#custom-props). |
For more details on how to build Custom Components, see [Building Custom Components](#building-custom-components).
### Import Map
In order for Payload to make use of [Component Paths](#component-paths), an "Import Map" is automatically generated at `app/(payload)/admin/importMap.js`. This file contains every Custom Component in your config, keyed to their respective paths. When Payload needs to lookup a component, it uses this file to find the correct import.
The Import Map is automatically regenerated at startup and whenever Hot Module Replacement (HMR) runs, or you can run `payload generate:importmap` to manually regenerate it.
#### Custom Imports
If needed, custom items can be appended onto the Import Map. This is mostly only relevant for plugin authors who need to add a custom import that is not referenced in a known location.
To add a custom import to the Import Map, use the `admin.dependencies` property in your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// ...
dependencies: {
myTestComponent: { // myTestComponent is the key - can be anything
path: '/components/TestComponent.js#TestComponent',
type: 'component',
clientProps: {
test: 'hello',
},
},
},
}
}
```
## Building Custom Components
All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default. This enables the use of the [Local API](../local-api/overview) directly on the front-end, among other things.
### Default Props
To make building Custom Components as easy as possible, Payload automatically provides common props, such as the [`payload`](../local-api/overview) class and the [`i18n`](../configuration/i18n) object. This means that when building Custom Components within the Admin Panel, you do not have to get these yourself.
Here is an example:
```tsx
import React from 'react'
const MyServerComponent = async ({
payload // highlight-line
}) => {
const page = await payload.findByID({
collection: 'pages',
id: '123',
})
return (
<p>{page.title}</p>
)
}
```
Each Custom Component receives the following props by default:
| Prop | Description |
| ------------------------- | ----------------------------------------------------------------------------------------------------- |
| `payload` | The [Payload](../local-api/overview) class. |
| `i18n` | The [i18n](../configuration/i18n) object. |
<Banner type="warning">
**Reminder:**
All Custom Components also receive various other props that are specific component being rendered. See [Root Components](#root-components), [Collection Components](./collections#custom-components), [Global Components](./globals#custom-components), or [Field Components](./fields#custom-components) for a complete list of all default props per component.
</Banner>
### Custom Props
To pass in custom props from the config, you can use either the `clientProps` or `serverProps` properties depending on whether your prop is [serializable](https://react.dev/reference/rsc/use-client#serializable-types), and whether your component is a Server or Client Component.
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: { // highlight-line
components: {
logout: {
Button: {
path: '/src/components/Logout#MyComponent',
clientProps: {
myCustomProp: 'Hello, World!' // highlight-line
},
}
}
}
},
})
```
```tsx
'use client'
import React from 'react'
export const MyComponent = ({ myCustomProp }: { myCustomProp: string }) => {
return (
<button>{myCustomProp}</button>
)
}
```
### Client Components
When [Building Custom Components](#building-custom-components), it's still possible to use client-side code such as `useState` or the `window` object. To do this, simply add the `use client` directive at the top of your file. Payload will automatically detect and remove all default, [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types) before rendering your component.
```tsx
'use client' // highlight-line
import React, { useState } from 'react'
export const MyClientComponent: React.FC = () => {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
)
}
```
<Banner type="warning">
**Reminder:**
Client Components cannot be passed [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types). If you are rendering your Client Component _from within_ a Server Component, ensure that its props are serializable.
</Banner>
### Accessing the Payload Config
From any Server Component, the [Payload Config](../configuration/overview) can be accessed directly from the `payload` prop:
```tsx
import React from 'react'
export default async function MyServerComponent({
payload: {
config // highlight-line
}
}) {
return (
<Link href={config.serverURL}>
Go Home
</Link>
)
}
```
But, the Payload Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) by design. It is full of custom validation functions, React components, etc. This means that the Payload Config, in its entirety, cannot be passed directly to Client Components.
For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](./hooks#useconfig) hook:
```tsx
'use client'
import React from 'react'
import { useConfig } from '@payloadcms/ui'
export const MyClientComponent: React.FC = () => {
const { config: { serverURL } } = useConfig() // highlight-line
return (
<Link href={serverURL}>
Go Home
</Link>
)
}
```
<Banner type="success">
See [Using Hooks](#using-hooks) for more details.
</Banner>
All [Field Components](./fields) automatically receive their respective Field Config through props.
```tsx
import React from 'react'
import type { TextFieldServerComponent } from 'payload'
export const MyClientFieldComponent: TextFieldServerComponent = ({ field: { name } }) => {
return (
<p>
{`This field's name is ${name}`}
</p>
)
}
```
### Getting the Current Language
All Custom Components can support multiple languages to be consistent with Payload's [Internationalization](../configuration/i18n). To do this, first add your translation resources to the [I18n Config](../configuration/i18n).
From any Server Component, you can translate resources using the `getTranslation` function from `@payloadcms/translations`. All Server Components automatically receive the `i18n` object as a prop by default.
```tsx
import React from 'react'
import { getTranslation } from '@payloadcms/translations'
export default async function MyServerComponent({ i18n }) {
const translatedTitle = getTranslation(myTranslation, i18n) // highlight-line
return (
<p>{translatedTitle}</p>
)
}
```
The best way to do this within a Client Component is to import the `useTranslation` hook from `@payloadcms/ui`:
```tsx
'use client'
import React from 'react'
import { useTranslation } from '@payloadcms/ui'
export const MyClientComponent: React.FC = () => {
const { t, i18n } = useTranslation() // highlight-line
return (
<ul>
<li>{t('namespace1:key', { variable: 'value' })}</li>
<li>{t('namespace2:key', { variable: 'value' })}</li>
<li>{i18n.language}</li>
</ul>
)
}
```
<Banner type="success">
See the [Hooks](./hooks) documentation for a full list of available hooks.
</Banner>
### Getting the Current Locale
All [Custom Views](./views) can support multiple locales to be consistent with Payload's [Localization](../configuration/localization). They automatically receive the `locale` object as a prop by default. This can be used to scope API requests, etc.:
```tsx
import React from 'react'
export default async function MyServerComponent({ payload, locale }) {
const localizedPage = await payload.findByID({
collection: 'pages',
id: '123',
locale,
})
return (
<p>{localizedPage.title}</p>
)
}
```
The best way to do this within a Client Component is to import the `useLocale` hook from `@payloadcms/ui`:
```tsx
'use client'
import React from 'react'
import { useLocale } from '@payloadcms/ui'
const Greeting: React.FC = () => {
const locale = useLocale() // highlight-line
const trans = {
en: 'Hello',
es: 'Hola',
}
return (
<span>{trans[locale.code]}</span>
)
}
```
<Banner type="success">
See the [Hooks](./hooks) documentation for a full list of available hooks.
</Banner>
### Using Hooks
To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](./hooks) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts. To do this, you can one of the many hooks available depending on your needs.
```tsx
'use client'
import React from 'react'
import { useDocumentInfo } from '@payloadcms/ui'
export const MyClientComponent: React.FC = () => {
const { slug } = useDocumentInfo() // highlight-line
return (
<p>{`Entity slug: ${slug}`}</p>
)
}
```
<Banner type="success">
See the [Hooks](./hooks) documentation for a full list of available hooks.
</Banner>
### Adding Styles
Payload has a robust [CSS Library](./customizing-css) that you can use to style your Custom Components similarly to Payload's built-in styling. This will ensure that your Custom Components match the existing design system, and so that they automatically adapt to any theme changes that might occur.
To apply custom styles, simply import your own `.css` or `.scss` file into your Custom Component:
```tsx
import './index.scss'
export const MyComponent: React.FC = () => {
return (
<div className="my-component">
My Custom Component
</div>
)
}
```
Then to colorize your Custom Component's background, for example, you can use the following CSS:
```scss
.my-component {
background-color: var(--theme-elevation-500);
}
```
Payload also exports its [SCSS](https://sass-lang.com) library for reuse which includes mixins, etc. To use this, simply import it as follows into your `.scss` file:
```scss
@import '~@payloadcms/ui/scss';
.my-component {
@include mid-break {
background-color: var(--theme-elevation-900);
}
}
```
<Banner type="success">
**Note:**
You can also drill into Payload's own component styles, or easily apply global, app-wide CSS. More on that [here](./customizing-css).
</Banner>
## Root Components
Root Components are those that effect the [Admin Panel](./overview) generally, such as the logo or the main nav.
To override Root Components, use the `admin.components` property in your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// highlight-start
components: {
// ...
},
// highlight-end
},
})
```
_For details on how to build Custom Components, see [Building Custom Components](#building-custom-components)._
The following options are available:
| Path | Description |
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`Nav`** | Contains the sidebar / mobile menu in its entirety. |
| **`beforeNavLinks`** | An array of Custom Components to inject into the built-in Nav, _before_ the links themselves. |
| **`afterNavLinks`** | An array of Custom Components to inject into the built-in Nav, _after_ the links. |
| **`beforeDashboard`** | An array of Custom Components to inject into the built-in Dashboard, _before_ the default dashboard contents. |
| **`afterDashboard`** | An array of Custom Components to inject into the built-in Dashboard, _after_ the default dashboard contents. |
| **`beforeLogin`** | An array of Custom Components to inject into the built-in Login, _before_ the default login form. |
| **`afterLogin`** | An array of Custom Components to inject into the built-in Login, _after_ the default login form. |
| **`logout.Button`** | The button displayed in the sidebar that logs the user out. |
| **`graphics.Icon`** | The simplified logo used in contexts like the the `Nav` component. |
| **`graphics.Logo`** | The full logo used in contexts like the `Login` view. |
| **`providers`** | Custom [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context) providers that will wrap the entire Admin Panel. [More details](#custom-providers). |
| **`actions`** | An array of Custom Components to be rendered _within_ the header of the Admin Panel, providing additional interactivity and functionality. |
| **`header`** | An array of Custom Components to be injected above the Payload header. |
| **`views`** | Override or create new views within the Admin Panel. [More details](./views). |
<Banner type="success">
**Note:**
You can also use set [Collection Components](./collections#custom-components) and [Global Components](./globals#custom-components) in their respective configs.
</Banner>
### Custom Providers
As you add more and more Custom Components to your [Admin Panel](./overview), you may find it helpful to add additional [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context)(s). Payload allows you to inject your own context providers in your app so you can export your own custom hooks, etc.
To add a Custom Provider, use the `admin.components.providers` property in your [Payload Config](../getting-started/overview):
```ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
components: {
providers: ['/path/to/MyProvider'], // highlight-line
},
},
})
```
Then build your Custom Provider as follows:
```tsx
'use client'
import React, { createContext, useContext } from 'react'
const MyCustomContext = React.createContext(myCustomValue)
export const MyProvider: React.FC = ({ children }) => {
return (
<MyCustomContext.Provider value={myCustomValue}>
{children}
</MyCustomContext.Provider>
)
}
export const useMyCustomContext = () => useContext(MyCustomContext)
```
<Banner type="warning">
**Reminder:**React Context exists only within Client Components. This means they must include the `use client` directive at the top of their files and cannot contain server-only code. To use a Server Component here, simply _wrap_ your Client Component with it.
</Banner>

View File

@@ -1,86 +0,0 @@
---
title: Customizing CSS & SCSS
label: Customizing CSS
order: 80
desc: Customize the Payload Admin Panel further by adding your own CSS or SCSS style sheet to the configuration, powerful theme and design options are waiting for you.
keywords: admin, css, scss, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Customizing the Payload [Admin Panel](./overview) through CSS alone is one of the easiest and most powerful ways to customize the look and feel of the dashboard. To allow for this level of customization, Payload:
1. Exposes a [root-level stylesheet](#global-css) for you to inject custom selectors
1. Provides a [CSS library](#css-library) that can be easily overridden or extended
1. Uses [BEM naming conventions](http://getbem.com) so that class names are globally accessible
To customize the CSS within the Admin UI, determine scope and change you'd like to make, and then add your own CSS or SCSS to the configuration as needed.
## Global CSS
Global CSS refers to the CSS that is applied to the entire [Admin Panel](./overview). This is where you can have a significant impact to the look and feel of the Admin UI through just a few lines of code.
You can add your own global CSS through the root `custom.scss` file of your app. This file is loaded into the root of the Admin Panel and can be used to inject custom selectors or styles however needed.
Here is an example of how you might target the Dashboard View and change the background color:
```scss
.dashboard {
background-color: red; // highlight-line
}
```
<Banner type="warning">
**Note:**
If you are building [Custom Components](./components), it is best to import your own stylesheets directly into your components, rather than using the global stylesheet. You can continue to use the [CSS library](#css-library) as needed.
</Banner>
### Specificity rules
All Payload CSS is encapsulated inside CSS layers under `@layer payload-default`. Any custom css will now have the highest possible specificity.
We have also provided a layer `@layer payload` if you want to use layers and ensure that your styles are applied after payload.
To override existing styles in a way that the previous rules of specificity would be respected you can use the default layer like so
```css
@layer payload-default {
// my styles within the payload specificity
}
```
## Re-using Payload SCSS variables and utilities
You can re-use Payload's SCSS variables and utilities in your own stylesheets by importing it from the UI package.
```scss
@import '~@payloadcms/ui/scss';
```
## CSS Library
To make it as easy as possible for you to override default styles, Payload uses [BEM naming conventions](http://getbem.com/) for all CSS within the Admin UI. If you provide your own CSS, you can override any built-in styles easily, including targeting nested components and their various component states.
You can also override Payload's built-in [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties). These variables are widely consumed by the Admin Panel, so modifying them has a significant impact on the look and feel of the Admin UI.
The following variables are defined and can be overridden:
- [Breakpoints](https://github.com/payloadcms/payload/blob/main/packages/ui/src/scss/queries.scss)
- [Colors](https://github.com/payloadcms/payload/blob/main/packages/ui/src/scss/colors.scss)
- Base color shades (white to black by default)
- Success / warning / error color shades
- Theme-specific colors (background, input background, text color, etc.)
- Elevation colors (used to determine how "bright" something should be when compared to the background)
- [Sizing](https://github.com/payloadcms/payload/blob/main/packages/ui/src/scss/app.scss)
- Horizontal gutter
- Transition speeds
- Font sizes
- Etc.
For an up-to-date, comprehensive list of all available variables, please refer to the [Source Code](https://github.com/payloadcms/payload/blob/main/packages/ui/src/scss).
<Banner type="warning">
**Warning:**
If you're overriding colors or theme elevations, make sure to consider how [your changes will affect dark mode](#dark-mode).
</Banner>
#### Dark Mode
Colors are designed to automatically adapt to theme of the [Admin Panel](./overview). By default, Payload automatically overrides all `--theme-elevation` colors and inverts all success / warning / error shades to suit dark mode. We also update some base theme variables like `--theme-bg`, `--theme-text`, etc.

View File

@@ -1,509 +0,0 @@
---
title: Customizing Fields
label: Customizing Fields
order: 60
desc:
keywords:
---
[Fields](../fields/overview) within the [Admin Panel](./overview) can be endlessly customized in their appearance and behavior without affecting their underlying data structure. Fields are designed to withstand heavy modification or even complete replacement through the use of [Custom Field Components](#custom-components), [Conditional Logic](#conditional-logic), [Custom Validations](../fields/overview#validation), and more.
For example, your app might need to render a specific interface that Payload does not inherently support, such as a color picker. To do this, you could replace the default [Text Field](../fields/text) input with your own user-friendly component that formats the data into a valid color value.
<Banner type="success">
**Tip:**
Don't see a built-in field type that you need? Build it! Using a combination of [Field Validations](../fields/overview#validation)
and [Custom Components](./components), you can override the entirety of how a component functions within the [Admin Panel](./overview) to effectively create your own field type.
</Banner>
## Admin Options
You can customize the appearance and behavior of fields within the [Admin Panel](./overview) through the `admin` property of any [Field Config](../fields/overview):
```ts
import type { CollectionConfig } from 'payload'
export const CollectionConfig: CollectionConfig = {
// ...
fields: [
// ...
{
name: 'myField',
type: 'text',
admin: { // highlight-line
// ...
},
}
]
}
```
The following options are available:
| Option | Description |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`condition`** | Programmatically show / hide fields based on other fields. [More details](../admin/fields#conditional-logic). |
| **`components`** | All Field Components can be swapped out for [Custom Components](../admin/components) that you define. [More details](../admin/fields). |
| **`description`** | Helper text to display alongside the field to provide more information for the editor. [More details](../admin/fields#description). |
| **`position`** | Specify if the field should be rendered in the sidebar by defining `position: 'sidebar'`. |
| **`width`** | Restrict the width of a field. You can pass any string-based value here, be it pixels, percentages, etc. This property is especially useful when fields are nested within a `Row` type where they can be organized horizontally. |
| **`style`** | [CSS Properties](https://developer.mozilla.org/en-US/docs/Web/CSS) to inject into the root element of the field. |
| **`className`** | Attach a [CSS class attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/Class_selectors) to the root DOM element of a field. |
| **`readOnly`** | Setting a field to `readOnly` has no effect on the API whatsoever but disables the admin component's editability to prevent editors from modifying the field's value. |
| **`disabled`** | If a field is `disabled`, it is completely omitted from the [Admin Panel](../admin/overview) entirely. |
| **`disableBulkEdit`** | Set `disableBulkEdit` to `true` to prevent fields from appearing in the select options when making edits for multiple documents. Defaults to `true` for UI fields. |
| **`disableListColumn`** | Set `disableListColumn` to `true` to prevent fields from appearing in the list view column selector. |
| **`disableListFilter`** | Set `disableListFilter` to `true` to prevent fields from appearing in the list view filter options. |
| **`hidden`** | Will transform the field into a `hidden` input type. Its value will still submit with requests in the Admin Panel, but the field itself will not be visible to editors. |
## Field Descriptions
Field Descriptions are used to provide additional information to the editor about a field, such as special instructions. Their placement varies from field to field, but typically are displayed with subtle style differences beneath the field inputs.
A description can be configured in three ways:
- As a string.
- As a function which returns a string. [More details](#description-functions).
- As a React component. [More details](#description).
To add a Custom Description to a field, use the `admin.description` property in your [Field Config](../fields/overview):
```ts
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
fields: [
// ...
{
name: 'myField',
type: 'text',
admin: {
description: 'Hello, world!' // highlight-line
},
},
]
}
```
<Banner type="warning">
**Reminder:**
To replace the Field Description with a [Custom Component](./components), use the `admin.components.Description` property. [More details](#description).
</Banner>
#### Description Functions
Custom Descriptions can also be defined as a function. Description Functions are executed on the server and can be used to format simple descriptions based on the user's current [Locale](../configuration/localization).
To add a Description Function to a field, set the `admin.description` property to a _function_ in your [Field Config](../fields/overview):
```ts
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
fields: [
// ...
{
name: 'myField',
type: 'text',
admin: {
description: ({ t }) => `${t('Hello, world!')}` // highlight-line
},
},
]
}
```
All Description Functions receive the following arguments:
| Argument | Description |
| -------------- | ---------------------------------------------------------------- |
| **`t`** | The `t` function used to internationalize the Admin Panel. [More details](../configuration/i18n) |
<Banner type="info">
**Note:**
If you need to subscribe to live updates within your form, use a Description Component instead. [More details](#description).
</Banner>
## Conditional Logic
You can show and hide fields based on what other fields are doing by utilizing conditional logic on a field by field basis. The `condition` property on a field's admin config accepts a function which takes three arguments:
- `data` - the entire document's data that is currently being edited
- `siblingData` - only the fields that are direct siblings to the field with the condition
- `{ user }` - the final argument is an object containing the currently authenticated user
The `condition` function should return a boolean that will control if the field should be displayed or not.
**Example:**
```ts
{
fields: [
{
name: 'enableGreeting',
type: 'checkbox',
defaultValue: false,
},
{
name: 'greeting',
type: 'text',
admin: {
// highlight-start
condition: (data, siblingData, { user }) => {
if (data.enableGreeting) {
return true
} else {
return false
}
},
// highlight-end
},
},
]
}
```
## Custom Components
Within the [Admin Panel](./overview), fields are represented in three distinct places:
- [Field](#field) - The actual form field rendered in the Edit View.
- [Cell](#cell) - The table cell component rendered in the List View.
- [Filter](#filter) - The filter component rendered in the List View.
To swap in Field Components with your own, use the `admin.components` property in your [Field Config](../fields/overview):
```ts
import type { CollectionConfig } from 'payload'
export const CollectionConfig: CollectionConfig = {
// ...
fields: [
// ...
{
// ...
admin: {
components: { // highlight-line
// ...
},
},
}
]
}
```
The following options are available:
| Component | Description |
| ---------- | --------------------------------------------------------------------------------------------------------------------------- |
| **`Field`** | The form field rendered of the Edit View. [More details](#field). |
| **`Cell`** | The table cell rendered of the List View. [More details](#cell). |
| **`Filter`** | The filter component rendered in the List View. [More details](#filter). |
| **`Label`** | Override the default Label of the Field Component. [More details](#label). |
| **`Error`** | Override the default Error of the Field Component. [More details](#error). |
| **`Description`** | Override the default Description of the Field Component. [More details](#description). |
| **`beforeInput`** | An array of elements that will be added before the input of the Field Component. [More details](#afterinput-and-beforeinput).|
| **`afterInput`** | An array of elements that will be added after the input of the Field Component. [More details](#afterinput-and-beforeinput). |
### Field
The Field Component is the actual form field rendered in the Edit View. This is the input that user's will interact with when editing a document.
To swap in your own Field Component, use the `admin.components.Field` property in your [Field Config](../fields/overview):
```ts
import type { CollectionConfig } from 'payload'
export const CollectionConfig: CollectionConfig = {
// ...
fields: [
// ...
{
// ...
admin: {
components: {
Field: '/path/to/MyFieldComponent', // highlight-line
},
},
}
]
}
```
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
<Banner type="warning">
Instead of replacing the entire Field Component, you can alternately replace or slot-in only specific parts by using the [`Label`](#label), [`Error`](#error), [`beforeInput`](#afterinput-and-beforinput), and [`afterInput`](#afterinput-and-beforinput) properties.
</Banner>
#### Default Props
All Field Components receive the following props by default:
| Property | Description |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`docPreferences`** | An object that contains the [Preferences](./preferences) for the document. |
| **`field`** | In Client Components, this is the sanitized Client Field Config. In Server Components, this is the original Field Config. Server Components will also receive the sanitized field config through the`clientField` prop (see below). |
| **`locale`** | The locale of the field. [More details](../configuration/localization). |
| **`readOnly`** | A boolean value that represents if the field is read-only or not. |
| **`user`** | The currently authenticated user. [More details](../authentication/overview). |
| **`validate`** | A function that can be used to validate the field. |
| **`path`** | A string representing the direct, dynamic path to the field at runtime, i.e. `myGroup.myArray.0.myField`. |
| **`schemaPath`** | A string representing the direct, static path to the [Field Config](../fields/overview), i.e. `posts.myGroup.myArray.myField`. |
| **`indexPath`** | A hyphen-notated string representing the path to the field _within the nearest named ancestor field_, i.e. `0-0` |
In addition to the above props, all Server Components will also receive the following props:
| Property | Description |
| ----------------- | ----------------------------------------------------------------------------- |
| **`clientField`** | The serializable Client Field Config. |
| **`field`** | The Field Config. [More details](../fields/overview). |
| **`data`** | The current document being edited. |
| **`i18n`** | The [i18n](../configuration/i18n) object. |
| **`payload`** | The [Payload](../local-api/overview) class. |
| **`permissions`** | The field permissions based on the currently authenticated user. |
| **`siblingData`** | The data of the field's siblings. |
| **`user`** | The currently authenticated user. [More details](../authentication/overview). |
| **`value`** | The value of the field at render-time. |
#### Sending and receiving values from the form
When swapping out the `Field` component, you are responsible for sending and receiving the field's `value` from the form itself.
To do so, import the [`useField`](./hooks#usefield) hook from `@payloadcms/ui` and use it to manage the field's value:
```tsx
'use client'
import { useField } from '@payloadcms/ui'
export const CustomTextField: React.FC = () => {
const { value, setValue } = useField() // highlight-line
return (
<input
onChange={(e) => setValue(e.target.value)}
value={value}
/>
)
}
```
<Banner type="success">
For a complete list of all available React hooks, see the [Payload React Hooks](./hooks) documentation. For additional help, see [Building Custom Components](./components#building-custom-components).
</Banner>
#### TypeScript
When building Custom Field Components, you can import the client field props to ensure type safety in your component. There is an explicit type for the Field Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to prepend the field type onto the target type, i.e. `TextFieldClientComponent`:
```tsx
import type {
TextFieldClientComponent,
TextFieldServerComponent,
TextFieldClientProps,
TextFieldServerProps,
// ...and so on for each Field Type
} from 'payload'
```
### Cell
The Cell Component is rendered in the table of the List View. It represents the value of the field when displayed in a table cell.
To swap in your own Cell Component, use the `admin.components.Cell` property in your [Field Config](../fields/overview):
```ts
import type { Field } from 'payload'
export const myField: Field = {
name: 'myField',
type: 'text',
admin: {
components: {
Cell: '/path/to/MyCustomCellComponent', // highlight-line
},
},
}
```
All Cell Components receive the same [Default Field Component Props](#field), plus the following:
| Property | Description |
| ---------------- | ----------------------------------------------------------------- |
| **`link`** | A boolean representing whether this cell should be wrapped in a link. |
| **`onClick`** | A function that is called when the cell is clicked. |
For details on how to build Custom Components themselves, see [Building Custom Components](./components#building-custom-components).
### Filter
The Filter Component is the actual input element rendered within the "Filter By" dropdown of the List View used to represent this field when building filters.
To swap in your own Filter Component, use the `admin.components.Filter` property in your [Field Config](../fields/overview):
```ts
import type { Field } from 'payload'
export const myField: Field = {
name: 'myField',
type: 'text',
admin: {
components: {
Filter: '/path/to/MyCustomFilterComponent', // highlight-line
},
},
}
```
All Custom Filter Components receive the same [Default Field Component Props](#field).
For details on how to build Custom Components themselves, see [Building Custom Components](./components#building-custom-components).
### Label
The Label Component is rendered anywhere a field needs to be represented by a label. This is typically used in the Edit View, but can also be used in the List View and elsewhere.
To swap in your own Label Component, use the `admin.components.Label` property in your [Field Config](../fields/overview):
```ts
import type { Field } from 'payload'
export const myField: Field = {
name: 'myField',
type: 'text',
admin: {
components: {
Label: '/path/to/MyCustomLabelComponent', // highlight-line
},
},
}
```
All Custom Label Components receive the same [Default Field Component Props](#field).
For details on how to build Custom Components themselves, see [Building Custom Components](./components#building-custom-components).
#### TypeScript
When building Custom Label Components, you can import the component types to ensure type safety in your component. There is an explicit type for the Label Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `LabelServerComponent` or `LabelClientComponent` to the type of field, i.e. `TextFieldLabelClientComponent`.
```tsx
import type {
TextFieldLabelServerComponent,
TextFieldLabelClientComponent,
// ...and so on for each Field Type
} from 'payload'
```
### Description
Alternatively to the [Description Property](#field-descriptions), you can also use a [Custom Component](./components) as the Field Description. This can be useful when you need to provide more complex feedback to the user, such as rendering dynamic field values or other interactive elements.
To add a Description Component to a field, use the `admin.components.Description` property in your [Field Config](../fields/overview):
```ts
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
fields: [
// ...
{
name: 'myField',
type: 'text',
admin: {
components: {
Description: '/path/to/MyCustomDescriptionComponent', // highlight-line
}
}
}
]
}
```
All Custom Description Components receive the same [Default Field Component Props](#field).
For details on how to build a Custom Components themselves, see [Building Custom Components](./components#building-custom-components).
#### TypeScript
When building Custom Description Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Description Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `DescriptionServerComponent` or `DescriptionClientComponent` to the type of field, i.e. `TextFieldDescriptionClientComponent`.
```tsx
import type {
TextFieldDescriptionServerComponent,
TextFieldDescriptionClientComponent,
// And so on for each Field Type
} from 'payload'
```
### Error
The Error Component is rendered when a field fails validation. It is typically displayed beneath the field input in a visually-compelling style.
To swap in your own Error Component, use the `admin.components.Error` property in your [Field Config](../fields/overview):
```ts
import type { Field } from 'payload'
export const myField: Field = {
name: 'myField',
type: 'text',
admin: {
components: {
Error: '/path/to/MyCustomErrorComponent', // highlight-line
},
},
}
```
All Error Components receive the [Default Field Component Props](#field).
For details on how to build Custom Components themselves, see [Building Custom Components](./components#building-custom-components).
#### TypeScript
When building Custom Error Components, you can import the component types to ensure type safety in your component. There is an explicit type for the Error Component, one for every [Field Type](../fields/overview) and server/client environment. The convention is to append `ErrorServerComponent` or `ErrorClientComponent` to the type of field, i.e. `TextFieldErrorClientComponent`.
```tsx
import type {
TextFieldErrorServerComponent,
TextFieldErrorClientComponent,
// And so on for each Field Type
} from 'payload'
```
### afterInput and beforeInput
With these properties you can add multiple components _before_ and _after_ the input element, as their name suggests. This is useful when you need to render additional elements alongside the field without replacing the entire field component.
To add components before and after the input element, use the `admin.components.beforeInput` and `admin.components.afterInput` properties in your [Field Config](../fields/overview):
```ts
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
fields: [
// ...
{
name: 'myField',
type: 'text',
admin: {
components: {
// highlight-start
beforeInput: ['/path/to/MyCustomComponent'],
afterInput: ['/path/to/MyOtherCustomComponent'],
// highlight-end
}
}
}
]
}
```
All `afterInput` and `beforeInput` Components receive the same [Default Field Component Props](#field).
For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components).

View File

@@ -1,107 +0,0 @@
---
title: Global Admin Config
label: Globals
order: 30
desc:
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
The behavior of [Globals](../configuration/globals) within the [Admin Panel](./overview) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](./components), setting page metadata, and more.
To configure Admin Options for Globals, use the `admin` property in your Global Config:
```ts
import { GlobalConfig } from 'payload'
export const MyGlobal: GlobalConfig = {
// ...
admin: { // highlight-line
// ...
},
}
```
## Admin Options
The following options are available:
| Option | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **`group`** | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide the link from the navigation while keeping its routes accessible. |
| **`hidden`** | Set to true or a function, called with the current user, returning true to exclude this Global from navigation and admin routing. |
| **`components`** | Swap in your own React components to be used within this Global. [More details](#custom-components). |
| **`preview`** | Function to generate a preview URL within the Admin Panel for this Global that can point to your app. [More details](#preview). |
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| **`hideAPIURL`** | Hides the "API URL" meta field while editing documents within this collection. |
| **`meta`** | Page metadata overrides to apply to this Global within the Admin Panel. [More details](./metadata). |
### Custom Components
Globals can set their own [Custom Components](./components) which only apply to [Global](../configuration/globals)-specific UI within the [Admin Panel](./overview). This includes elements such as the Save Button, or entire layouts such as the Edit View.
To override Global Components, use the `admin.components` property in your [Global Config](../configuration/globals):
```ts
import type { SanitizedGlobalConfig } from 'payload'
export const MyGlobal: SanitizedGlobalConfig = {
// ...
admin: {
components: { // highlight-line
// ...
},
},
}
```
The following options are available:
| Path | Description |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| **`elements.SaveButton`** | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. |
| **`elements.SaveDraftButton`** | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. |
| **`elements.PublishButton`** | Replace the default Publish Button with a Custom Component. [Drafts](../versions/drafts) must be enabled. |
| **`elements.PreviewButton`** | Replace the default Preview Button with a Custom Component. [Preview](#preview) must be enabled. |
| **`views`** | Override or create new views within the Admin Panel. [More details](./views). |
<Banner type="success">
**Note:**
For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components).
</Banner>
### Preview
It is possible to display a Preview Button within the Edit View of the Admin Panel. This will allow editors to visit the frontend of your app the corresponds to the document they are actively editing. This way they can preview the latest, potentially unpublished changes.
To configure the Preview Button, set the `admin.preview` property to a function in your Global Config:
```ts
import { GlobalConfig } from 'payload'
export const MainMenu: GlobalConfig = {
// ...
admin: {
// highlight-start
preview: (doc, { locale }) => {
if (doc?.slug) {
return `/${doc.slug}?locale=${locale}`
}
return null
},
// highlight-end
},
}
```
The preview function receives two arguments:
| Argument | Description |
| --- | --- |
| **`doc`** | The Document being edited. |
| **`ctx`** | An object containing `locale` and `token` properties. The `token` is the currently logged-in user's JWT. |
<Banner type="success">
**Note:**
For fully working example of this, check of the official [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview) in the [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples).
</Banner>

View File

@@ -1,950 +0,0 @@
---
title: React Hooks
label: React Hooks
order: 70
desc: Make use of all of the powerful React hooks that Payload provides.
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Payload provides a variety of powerful [React Hooks](https://react.dev/reference/react-dom/hooks) that can be used within your own [Custom Components](./components), such as [Custom Fields](./fields). With them, you can interface with Payload itself to build just about any type of complex customization you can think of.
<Banner type="warning">
**Reminder:**
All Custom Components are [React Server Components](https://react.dev/reference/rsc/server-components) by default. Hooks, on the other hand, are only available in client-side environments. To use hooks, [ensure your component is a client component](./components#client-components).
</Banner>
## useField
The `useField` hook is used internally within all field components. It manages sending and receiving a field's state from its parent form. When you build a [Custom Field Component](./fields), you will be responsible for sending and receiving the field's `value` to and from the form yourself.
To do so, import the `useField` hook as follows:
```tsx
'use client'
import type { TextFieldClientComponent } from 'payload'
import { useField } from '@payloadcms/ui'
export const CustomTextField: TextFieldClientComponent = ({ path }) => {
const { value, setValue } = useField({ path }) // highlight-line
return (
<div>
<p>
{path}
</p>
<input
onChange={(e) => { setValue(e.target.value) }}
value={value}
/>
</div>
)
}
```
The `useField` hook accepts the following arguments:
| Property | Description |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `path` | If you do not provide a `path`, `name` will be used instead. This is the path to the field in the form data. |
| `validate` | A validation function executed client-side _before_ submitting the form to the server. Different than [Field-level Validation](../fields/overview#validation) which runs strictly on the server. |
| `disableFormData` | If `true`, the field will not be included in the form data when the form is submitted. |
| `hasRows` | If `true`, the field will be treated as a field with rows. This is useful for fields like `array` and `blocks`. |
The `useField` hook returns the following object:
```ts
type FieldType<T> = {
errorMessage?: string
errorPaths?: string[]
filterOptions?: FilterOptionsResult
formInitializing: boolean
formProcessing: boolean
formSubmitted: boolean
initialValue?: T
path: string
permissions: FieldPermissions
readOnly?: boolean
rows?: Row[]
schemaPath: string
setValue: (val: unknown, disableModifyingForm?: boolean) => void
showError: boolean
valid?: boolean
value: T
}
```
## useFormFields
There are times when a custom field component needs to have access to data from other fields, and you have a few options to do so. The `useFormFields` hook is a powerful and highly performant way to retrieve a form's field state, as well as to retrieve the `dispatchFields` method, which can be helpful for setting other fields' form states from anywhere within a form.
<Banner type="success">
**This hook is great for retrieving only certain fields from form state** because it
ensures that it will only cause a rerender when the items that you ask for change.
</Banner>
Thanks to the awesome package [`use-context-selector`](https://github.com/dai-shi/use-context-selector), you can retrieve a specific field's state easily. This is ideal because you can ensure you have an up-to-date field state, and your component will only re-render when _that field's state_ changes.
You can pass a Redux-like selector into the hook, which will ensure that you retrieve only the field that you want. The selector takes an argument with type of `[fields: Fields, dispatch: React.Dispatch<Action>]]`.
```tsx
'use client'
import { useFormFields } from '@payloadcms/ui'
const MyComponent: React.FC = () => {
// Get only the `amount` field state, and only cause a rerender when that field changes
const amount = useFormFields(([fields, dispatch]) => fields.amount)
// Do the same thing as above, but to the `feePercentage` field
const feePercentage = useFormFields(([fields, dispatch]) => fields.feePercentage)
if (typeof amount?.value !== 'undefined' && typeof feePercentage?.value !== 'undefined') {
return <span>The fee is ${(amount.value * feePercentage.value) / 100}</span>
}
}
```
## useAllFormFields
**To retrieve more than one field**, you can use the `useAllFormFields` hook. Your component will re-render when _any_ field changes, so use this hook only if you absolutely need to. Unlike the `useFormFields` hook, this hook does not accept a "selector", and it always returns an array with type of `[fields: Fields, dispatch: React.Dispatch<Action>]]`.
You can do lots of powerful stuff by retrieving the full form state, like using built-in helper functions to reduce field state to values only, or to retrieve sibling data by path.
```tsx
'use client'
import { useAllFormFields } from '@payloadcms/ui'
import { reduceFieldsToValues, getSiblingData } from 'payload/shared'
const ExampleComponent: React.FC = () => {
// the `fields` const will be equal to all fields' state,
// and the `dispatchFields` method is usable to send field state up to the form
const [fields, dispatchFields] = useAllFormFields();
// Pass in fields, and indicate if you'd like to "unflatten" field data.
// The result below will reflect the data stored in the form at the given time
const formData = reduceFieldsToValues(fields, true);
// Pass in field state and a path,
// and you will be sent all sibling data of the path that you've specified
const siblingData = getSiblingData(fields, 'someFieldName');
return (
// return some JSX here if necessary
)
};
```
#### Updating other fields' values
If you are building a Custom Component, then you should use `setValue` which is returned from the `useField` hook to programmatically set your field's value. But if you're looking to update _another_ field's value, you can use `dispatchFields` returned from `useFormFields`.
You can send the following actions to the `dispatchFields` function.
| Action | Description |
| ---------------------- | -------------------------------------------------------------------------- |
| **`ADD_ROW`** | Adds a row of data (useful in array / block field data) |
| **`DUPLICATE_ROW`** | Duplicates a row of data (useful in array / block field data) |
| **`MODIFY_CONDITION`** | Updates a field's conditional logic result (true / false) |
| **`MOVE_ROW`** | Moves a row of data (useful in array / block field data) |
| **`REMOVE`** | Removes a field from form state |
| **`REMOVE_ROW`** | Removes a row of data from form state (useful in array / block field data) |
| **`REPLACE_STATE`** | Completely replaces form state |
| **`UPDATE`** | Update any property of a specific field's state |
To see types for each action supported within the `dispatchFields` hook, check out the Form types [here](https://github.com/payloadcms/payload/blob/main/packages/ui/src/forms/Form/types.ts).
## useForm
The `useForm` hook can be used to interact with the form itself, and sends back many methods that can be used to reactively fetch form state without causing rerenders within your components each time a field is changed. This is useful if you have action-based callbacks that your components fire, and need to interact with form state _based on a user action_.
<Banner type="warning">
**Warning:**
This hook is optimized to avoid causing rerenders when fields change, and as such, its `fields`
property will be out of date. You should only leverage this hook if you need to perform actions
against the form in response to your users' actions. Do not rely on its returned "fields" as being
up-to-date. They will be removed from this hook's response in an upcoming version.
</Banner>
The `useForm` hook returns an object with the following properties:
<TableWithDrawers
columns={[
'Action',
'Description',
'Example',
]}
rows={[
[
{
value: "**`fields`**",
},
{
value: "Deprecated. This property cannot be relied on as up-to-date.",
},
{
value: ''
}
],
[
{
value: "**`submit`**",
},
{
value: "Method to trigger the form to submit",
},
{
value: ''
}
],
[
{
value: "**`dispatchFields`**",
},
{
value: "Dispatch actions to the form field state",
},
{
value: ''
}
],
[
{
value: "**`validateForm`**",
},
{
value: "Trigger a validation of the form state",
},
{
value: ''
}
],
[
{
value: "**`createFormData`**",
},
{
value: "Create a `multipart/form-data` object from the current form's state",
},
{
value: ''
}
],
[
{
value: "**`disabled`**",
},
{
value: "Boolean denoting whether or not the form is disabled",
},
{
value: ''
}
],
[
{
value: "**`getFields`**",
},
{
value: 'Gets all fields from state',
},
{
value: '',
}
],
[
{
value: "**`getField`**",
},
{
value: 'Gets a single field from state by path',
},
{
value: '',
},
],
[
{
value: "**`getData`**",
},
{
value: 'Returns the data stored in the form',
},
{
value: '',
},
],
[
{
value: "**`getSiblingData`**",
},
{
value: 'Returns form sibling data for the given field path',
},
{
value: '',
},
],
[
{
value: "**`setModified`**",
},
{
value: "Set the form\'s `modified` state",
},
{
value: '',
},
],
[
{
value: "**`setProcessing`**",
},
{
value: "Set the form\'s `processing` state",
},
{
value: '',
},
],
[
{
value: "**`setSubmitted`**",
},
{
value: "Set the form\'s `submitted` state",
},
{
value: '',
},
],
[
{
value: "**`formRef`**",
},
{
value: 'The ref from the form HTML element',
},
{
value: '',
},
],
[
{
value: "**`reset`**",
},
{
value: 'Method to reset the form to its initial state',
},
{
value: '',
},
],
[
{
value: "**`addFieldRow`**",
},
{
value: "Method to add a row on an array or block field",
},
{
drawerTitle: 'addFieldRow',
drawerDescription: 'A useful method to programmatically add a row to an array or block field.',
drawerSlug: 'addFieldRow',
drawerContent: `
<TableWithDrawers
columns={[
'Prop',
'Description',
]}
rows={[
[
{
value: "**\\\`path\\\`**",
},
{
value: "The path to the array or block field",
},
],
[
{
value: "**\\\`rowIndex\\\`**",
},
{
value: "The index of the row to add. If omitted, the row will be added to the end of the array.",
},
],
[
{
value: "**\\\`data\\\`**",
},
{
value: "The data to add to the row",
},
],
]}
/>
\`\`\`tsx
import { useForm } from "@payloadcms/ui"
export const CustomArrayManager = () => {
const { addFieldRow } = useForm()
return (
<button
type="button"
onClick={() => {
addFieldRow({
path: "arrayField",
schemaPath: "arrayField",
rowIndex: 0, // optionally specify the index to add the row at
subFieldState: {
textField: {
initialValue: 'New row text',
valid: true,
value: 'New row text',
},
},
// blockType: "yourBlockSlug",
// ^ if managing a block array, you need to specify the block type
})
}}
>
Add Row
</button>
)
}
\`\`\`
An example config to go along with the Custom Component
\`\`\`tsx
const ExampleCollection = {
slug: "example-collection",
fields: [
{
name: "arrayField",
type: "array",
fields: [
{
name: "textField",
type: "text",
},
],
},
{
type: "ui",
name: "customArrayManager",
admin: {
components: {
Field: '/path/to/CustomArrayManagerField',
},
},
},
],
}
\`\`\`
`
}
],
[
{
value: "**`removeFieldRow`**",
},
{
value: "Method to remove a row from an array or block field",
},
{
drawerTitle: 'removeFieldRow',
drawerDescription: 'A useful method to programmatically remove a row from an array or block field.',
drawerSlug: 'removeFieldRow',
drawerContent: `
<TableWithDrawers
columns={[
'Prop',
'Description',
]}
rows={[
[
{
value: "**\\\`path\\\`**",
},
{
value: "The path to the array or block field",
},
],
[
{
value: "**\\\`rowIndex\\\`**",
},
{
value: "The index of the row to remove",
},
],
]}
/>
\`\`\`tsx
import { useForm } from "@payloadcms/ui"
export const CustomArrayManager = () => {
const { removeFieldRow } = useForm()
return (
<button
type="button"
onClick={() => {
removeFieldRow({
path: "arrayField",
rowIndex: 0,
})
}}
>
Remove Row
</button>
)
}
\`\`\`
An example config to go along with the Custom Component
\`\`\`tsx
const ExampleCollection = {
slug: "example-collection",
fields: [
{
name: "arrayField",
type: "array",
fields: [
{
name: "textField",
type: "text",
},
],
},
{
type: "ui",
name: "customArrayManager",
admin: {
components: {
Field: '/path/to/CustomArrayManagerField',
},
},
},
],
}
\`\`\`
`
}
],
[
{
value: "**`replaceFieldRow`**",
},
{
value: "Method to replace a row from an array or block field",
},
{
drawerTitle: 'replaceFieldRow',
drawerDescription: 'A useful method to programmatically replace a row from an array or block field.',
drawerSlug: 'replaceFieldRow',
drawerContent: `
<TableWithDrawers
columns={[
'Prop',
'Description',
]}
rows={[
[
{
value: "**\\\`path\\\`**",
},
{
value: "The path to the array or block field",
},
],
[
{
value: "**\\\`rowIndex\\\`**",
},
{
value: "The index of the row to replace",
},
],
[
{
value: "**\\\`data\\\`**",
},
{
value: "The data to replace within the row",
},
],
]}
/>
\`\`\`tsx
import { useForm } from "@payloadcms/ui"
export const CustomArrayManager = () => {
const { replaceFieldRow } = useForm()
return (
<button
type="button"
onClick={() => {
replaceFieldRow({
path: "arrayField",
schemaPath: "arrayField",
rowIndex: 0, // optionally specify the index to add the row at
subFieldState: {
textField: {
initialValue: 'Updated text',
valid: true,
value: 'Upddated text',
},
},
// blockType: "yourBlockSlug",
// ^ if managing a block array, you need to specify the block type
})
}}
>
Replace Row
</button>
)
}
\`\`\`
An example config to go along with the Custom Component
\`\`\`tsx
const ExampleCollection = {
slug: "example-collection",
fields: [
{
name: "arrayField",
type: "array",
fields: [
{
name: "textField",
type: "text",
},
],
},
{
type: "ui",
name: "customArrayManager",
admin: {
components: {
Field: '/path/to/CustomArrayManagerField',
},
},
},
],
}
\`\`\`
`
}
],
]}
/>
## useCollapsible
The `useCollapsible` hook allows you to control parent collapsibles:
| Property | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **`isCollapsed`** | State of the collapsible. `true` if open, `false` if collapsed. |
| **`isVisible`** | If nested, determine if the nearest collapsible is visible. `true` if no parent is closed, `false` otherwise. |
| **`toggle`** | Toggles the state of the nearest collapsible. |
| **`isWithinCollapsible`** | Determine when you are within another collapsible. |
**Example:**
```tsx
'use client'
import React from 'react'
import { useCollapsible } from '@payloadcms/ui'
const CustomComponent: React.FC = () => {
const { isCollapsed, toggle } = useCollapsible()
return (
<div>
<p className="field-type">I am {isCollapsed ? 'closed' : 'open'}</p>
<button onClick={toggle} type="button">
Toggle
</button>
</div>
)
}
```
## useDocumentInfo
The `useDocumentInfo` hook provides information about the current document being edited, including the following:
| Property | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| **`currentEditor`** | The user currently editing the document. |
| **`docConfig`** | Either the Collection or Global config of the document, depending on what is being edited. |
| **`documentIsLocked`** | Whether the document is currently locked by another user. |
| **`id`** | If the doc is a collection, its ID will be returned |
| **`getDocPermissions`** | Method to retrieve document-level user preferences. |
| **`getDocPreferences`** | Method to retrieve document-level user preferences. |
| **`hasPublishedDoc`** | Whether the document has a published version. |
| **`incrementVersionCount`** | Method to increment the version count of the document. |
| **`preferencesKey`** | The `preferences` key to use when interacting with document-level user preferences. |
| **`versions`** | Versions of the current doc. |
| **`unpublishedVersions`** | Unpublished versions of the current doc. |
| **`publishedDoc`** | The currently published version of the doc being edited. |
| **`getVersions`** | Method to retrieve document versions. |
| **`docPermissions`** | The current documents permissions. Collection document permissions fallback when no id is present (i.e. on create). |
| **`versionCount`** | The current version count of the document. |
**Example:**
```tsx
'use client'
import { useDocumentInfo } from '@payloadcms/ui'
const LinkFromCategoryToPosts: React.FC = () => {
// highlight-start
const { id } = useDocumentInfo()
// highlight-end
// id will be undefined on the create form
if (!id) {
return null
}
return (
<a href={`/admin/collections/posts?where[or][0][and][0][category][in][0]=[${id}]`}>
View posts
</a>
)
}
```
## useListQuery
The `useListQuery` hook is used to subscribe to the data, current query, and other properties used within the List View. You can use this hook within any Custom Component rendered within the List View.
```tsx
'use client'
import { useListQuery } from '@payloadcms/ui'
const MyComponent: React.FC = () => {
// highlight-start
const { data, query } = useListQuery()
// highlight-end
// ...
}
```
The `useListQuery` hook returns an object with the following properties:
| Property | Description |
| ----------------- | --------------------------------------------------------------------------------------------- |
| **`data`** | The data that is being displayed in the List View. |
| **`defaultLimit`**| The default limit of items to display in the List View. |
| **`defaultSort`** | The default sort order of items in the List View. |
| **`handlePageChange`** | A method to handle page changes in the List View. |
| **`handlePerPageChange`** | A method to handle per page changes in the List View. |
| **`handleSearchChange`** | A method to handle search changes in the List View. |
| **`handleSortChange`** | A method to handle sort changes in the List View. |
| **`handleWhereChange`** | A method to handle where changes in the List View. |
| **`query`** | The current query that is being used to fetch the data in the List View. |
## useLocale
In any Custom Component you can get the selected locale object with the `useLocale` hook. `useLocale` gives you the full locale object, consisting of a `label`, `rtl`(right-to-left) property, and then `code`. Here is a simple example:
```tsx
'use client'
import { useLocale } from '@payloadcms/ui'
const Greeting: React.FC = () => {
// highlight-start
const locale = useLocale()
// highlight-end
const trans = {
en: 'Hello',
es: 'Hola',
}
return <span> {trans[locale.code]} </span>
}
```
## useAuth
Useful to retrieve info about the currently logged in user as well as methods for interacting with it. It sends back an object with the following properties:
| Property | Description |
| ------------------------ | --------------------------------------------------------------------------------------- |
| **`user`** | The currently logged in user |
| **`logOut`** | A method to log out the currently logged in user |
| **`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) |
| **`permissions`** | The permissions of the current user |
```tsx
'use client'
import { useAuth } from '@payloadcms/ui'
import type { User } from '../payload-types.ts'
const Greeting: React.FC = () => {
// highlight-start
const { user } = useAuth<User>()
// highlight-end
return <span>Hi, {user.email}!</span>
}
```
## useConfig
Used to retrieve the Payload [Client Config](./components#accessing-the-payload-config).
```tsx
'use client'
import { useConfig } from '@payloadcms/ui'
const MyComponent: React.FC = () => {
// highlight-start
const { config } = useConfig()
// highlight-end
return <span>{config.serverURL}</span>
}
```
If you need to retrieve a specific collection or global config by its slug, `getEntityConfig` is the most efficient way to do so:
```tsx
'use client'
import { useConfig } from '@payloadcms/ui'
const MyComponent: React.FC = () => {
// highlight-start
const { getEntityConfig } = useConfig()
const mediaConfig = getEntityConfig({ collectionSlug: 'media'})
// highlight-end
return <span>The media collection has {mediaConfig.fields.length} fields.</span>
}
```
## useEditDepth
Sends back how many editing levels "deep" the current component is. Edit depth is relevant while adding new documents / editing documents in modal windows and other cases.
```tsx
'use client'
import { useEditDepth } from '@payloadcms/ui'
const MyComponent: React.FC = () => {
// highlight-start
const editDepth = useEditDepth()
// highlight-end
return <span>My component is {editDepth} levels deep</span>
}
```
## usePreferences
Returns methods to set and get user preferences. More info can be found [here](https://payloadcms.com/docs/admin/preferences).
## useTheme
Returns the currently selected theme (`light`, `dark` or `auto`), a set function to update it and a boolean `autoMode`, used to determine if the theme value should be set automatically based on the user's device preferences.
```tsx
'use client'
import { useTheme } from '@payloadcms/ui'
const MyComponent: React.FC = () => {
// highlight-start
const { autoMode, setTheme, theme } = useTheme()
// highlight-end
return (
<>
<span>
The current theme is {theme} and autoMode is {autoMode}
</span>
<button
type="button"
onClick={() => setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))}
>
Toggle theme
</button>
</>
)
}
```
## useTableColumns
Returns methods to manipulate table columns
```tsx
'use client'
import { useTableColumns } from '@payloadcms/ui'
const MyComponent: React.FC = () => {
// highlight-start
const { setActiveColumns } = useTableColumns()
const resetColumns = () => {
setActiveColumns(['id', 'createdAt', 'updatedAt'])
}
// highlight-end
return (
<button type="button" onClick={resetColumns}>
Reset columns
</button>
)
}
```
## useDocumentEvents
The `useDocumentEvents` hook provides a way of subscribing to cross-document events, such as updates made to nested documents within a drawer. This hook will report document events that are outside the scope of the document currently being edited. This hook provides the following:
| Property | Description |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **`mostRecentUpdate`** | An object containing the most recently updated document. It contains the `entitySlug`, `id` (if collection), and `updatedAt` properties |
| **`reportUpdate`** | A method used to report updates to documents. It accepts the same arguments as the `mostRecentUpdate` property. |
**Example:**
```tsx
'use client'
import { useDocumentEvents } from '@payloadcms/ui'
const ListenForUpdates: React.FC = () => {
const { mostRecentUpdate } = useDocumentEvents()
return <span>{JSON.stringify(mostRecentUpdate)}</span>
}
```
<Banner type="info">
Right now the `useDocumentEvents` hook only tracks recently updated documents, but in the future
it will track more document-related events as needed, such as document creation, deletion, etc.
</Banner>

View File

@@ -1,79 +0,0 @@
---
title: Document Locking
label: Document Locking
order: 90
desc: Ensure your documents are locked during editing to prevent concurrent changes from multiple users and maintain data integrity.
keywords: locking, document locking, edit locking, document, concurrency, Payload, headless, Content Management System, cms, javascript, react, node, nextjs
---
Document locking in Payload ensures that only one user at a time can edit a document, preventing data conflicts and accidental overwrites. When a document is locked, other users are prevented from making changes until the lock is released, ensuring data integrity in collaborative environments.
The lock is automatically triggered when a user begins editing a document within the Admin Panel and remains in place until the user exits the editing view or the lock expires due to inactivity.
## How it works
When a user starts editing a document, Payload locks it for that user. If another user attempts to access the same document, they will be notified that it is currently being edited. They can then choose one of the following options:
- View in Read-Only: View the document without the ability to make any changes.
- Take Over: Take over editing from the current user, which locks the document for the new editor and notifies the original user.
- Return to Dashboard: Navigate away from the locked document and continue with other tasks.
The lock will automatically expire after a set period of inactivity, configurable using the `duration` property in the `lockDocuments` configuration, after which others can resume editing.
<Banner type="info"> **Note:** If your application does not require document locking, you can disable this feature for any collection or global by setting the `lockDocuments` property to `false`. </Banner>
### Config Options
The `lockDocuments` property exists on both the Collection Config and the Global Config. Document locking is enabled by default, but you can customize the lock duration or turn off the feature for any collection or global.
Here's an example configuration for document locking:
```ts
import type { CollectionConfig } from 'payload'
export const Posts: CollectionConfig = {
slug: 'posts',
fields: [
{
name: 'title',
type: 'text',
},
// other fields...
],
lockDocuments: {
duration: 600, // Duration in seconds
},
}
```
#### Locking Options
| Option | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`lockDocuments`** | Enables or disables document locking for the collection or global. By default, document locking is enabled. Set to an object to configure, or set to false to disable locking. |
| **`duration`** | Specifies the duration (in seconds) for how long a document remains locked without user interaction. The default is 300 seconds (5 minutes). |
### Impact on APIs
Document locking affects both the Local and REST APIs, ensuring that if a document is locked, concurrent users will not be able to perform updates or deletes on that document (including globals). If a user attempts to update or delete a locked document, they will receive an error.
Once the document is unlocked or the lock duration has expired, other users can proceed with updates or deletes as normal.
#### Overriding Locks
For operations like `update` and `delete`, Payload includes an `overrideLock` option. This boolean flag, when set to `false`, enforces document locks, ensuring that the operation will not proceed if another user currently holds the lock.
By default, `overrideLock` is set to `true`, which means that document locks are ignored, and the operation will proceed even if the document is locked. To enforce locks and prevent updates or deletes on locked documents, set `overrideLock: false`.
```ts
const result = await payload.update({
collection: 'posts',
id: '123',
data: {
title: 'New title',
},
overrideLock: false, // Enforces the document lock, preventing updates if the document is locked
})
```
This option is particularly useful in scenarios where administrative privileges or specific workflows require you to override the lock and ensure the operation is completed.

View File

@@ -1,216 +0,0 @@
---
title: Page Metadata
label: Metadata
order: 70
desc: Customize the metadata of your pages within the Admin Panel
keywords: admin, components, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Every page within the Admin Panel automatically receives dynamic, auto-generated metadata derived from live document data, the user's current locale, and more, without any additional configuration. This includes the page title, description, og:image and everything in between. Metadata is fully configurable at the root level and cascades down to individual collections, documents, and custom views, allowing for the ability to control metadata on any page with high precision.
Within the Admin Panel, metadata can be customized at the following levels:
- [Root Metadata](#root-metadata)
- [Collection Metadata](#collection-metadata)
- [Global Metadata](#global-metadata)
- [View Metadata](#view-metadata)
All of these types of metadata share a similar structure, with a few key differences on the Root level. To customize metadata, consult the list of available scopes. Determine the scope that corresponds to what you are trying to accomplish, then author your metadata within the Payload Config accordingly.
## Root Metadata
Root Metadata is the metadata that is applied to all pages within the Admin Panel. This is where you can control things like the suffix appended onto each page's title, the favicon displayed in the browser's tab, and the Open Graph data that is used when sharing the Admin Panel on social media.
To customize Root Metadata, use the `admin.meta` key in your Payload Config:
```ts
{
// ...
admin: {
// highlight-start
meta: {
// highlight-end
title: 'My Admin Panel',
description: 'The best admin panel in the world',
icons: [
{
rel: 'icon',
type: 'image/png',
url: '/favicon.png',
},
],
},
},
}
```
The following options are available for Root Metadata:
| Key | Type | Description |
| --- | --- | --- |
| **`title`** | `string` | The title of the Admin Panel. |
| **`description`** | `string` | The description of the Admin Panel. |
| **`defaultOGImageType`** | `dynamic` (default), `static`, or `off` | The type of default OG image to use. If set to `dynamic`, Payload will use Next.js image generation to create an image with the title of the page. If set to `static`, Payload will use the `defaultOGImage` URL. If set to `off`, Payload will not generate an OG image. |
| **`icons`** | `IconConfig[]` | An array of icon objects. [More details](#icons) |
| **`keywords`** | `string` | A comma-separated list of keywords to include in the metadata of the Admin Panel. |
| **`openGraph`** | `OpenGraphConfig` | An object containing Open Graph metadata. [More details](#open-graph) |
| **`titleSuffix`** | `string` | A suffix to append to the end of the title of every page. Defaults to "- Payload". |
<Banner type="success">
**Reminder:**
These are the _root-level_ options for the Admin Panel. You can also customize [Collection Metadata](./collections), [Global Metadata](./globals), and [Document Metadata](./documents) in their respective configs.
</Banner>
### Icons
The Icons Config corresponds to the `<link>` tags that are used to specify icons for the Admin Panel. The `icons` key is an array of objects, each of which represents an individual icon. Icons are differentiated from one another by their `rel` attribute, which specifies the relationship between the document and the icon.
The most common icon type is the favicon, which is displayed in the browser tab. This is specified by the `rel` attribute `icon`. Other common icon types include `apple-touch-icon`, which is used by Apple devices when the Admin Panel is saved to the home screen, and `mask-icon`, which is used by Safari to mask the Admin Panel icon.
To customize icons, use the `icons` key within the `admin.meta` object in your Payload Config:
```ts
{
// ...
admin: {
meta: {
// highlight-start
icons: [
// highlight-end
{
rel: 'icon',
type: 'image/png',
url: '/favicon.png',
},
{
rel: 'apple-touch-icon',
type: 'image/png',
url: '/apple-touch-icon.png',
},
],
},
},
}
```
The following options are available for Icons:
| Key | Type | Description |
| --- | --- | --- |
| **`rel`** | `string` | The HTML `rel` attribute of the icon. |
| **`type`** | `string` | The MIME type of the icon. |
| **`color`** | `string` | The color of the icon. |
| **`fetchPriority`** | `string` | The [fetch priority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) of the icon. |
| **`media`** | `string` | The [media query](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries) of the icon. |
| **`sizes`** | `string` | The [sizes](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/sizes) of the icon. |
| **`url`** | `string` | The URL pointing the resource of the icon. |
### Open Graph
Open Graph metadata is a set of tags that are used to control how URLs are displayed when shared on social media platforms. Open Graph metadata is automatically generated by Payload, but can be customized at the Root level.
To customize Open Graph metadata, use the `openGraph` key within the `admin.meta` object in your Payload Config:
```ts
{
// ...
admin: {
meta: {
// highlight-start
openGraph: {
// highlight-end
description: 'The best admin panel in the world',
images: [
{
url: 'https://example.com/image.jpg',
width: 800,
height: 600,
},
],
siteName: 'Payload',
title: 'My Admin Panel',
},
},
},
}
```
The following options are available for Open Graph Metadata:
| Key | Type | Description |
| --- | --- | --- |
| **`description`** | `string` | The description of the Admin Panel. |
| **`images`** | `OGImageConfig` or `OGImageConfig[]` | An array of image objects. |
| **`siteName`** | `string` | The name of the site. |
| **`title`** | `string` | The title of the Admin Panel. |
## Collection Metadata
Collection Metadata is the metadata that is applied to all pages within any given Collection within the Admin Panel. This metadata is used to customize the title and description of all views within any given Collection, unless overridden by the view itself.
To customize Collection Metadata, use the `admin.meta` key within your Collection Config:
```ts
import type { CollectionConfig } from 'payload'
export const MyCollection: CollectionConfig = {
// ...
admin: {
// highlight-start
meta: {
// highlight-end
title: 'My Collection',
description: 'The best collection in the world',
},
},
}
```
The Collection Meta config has the same options as the [Root Metadata](#root-metadata) config.
## Global Metadata
Global Metadata is the metadata that is applied to all pages within any given Global within the Admin Panel. This metadata is used to customize the title and description of all views within any given Global, unless overridden by the view itself.
To customize Global Metadata, use the `admin.meta` key within your Global Config:
```ts
import { GlobalConfig } from 'payload'
export const MyGlobal: GlobalConfig = {
// ...
admin: {
// highlight-start
meta: {
// highlight-end
title: 'My Global',
description: 'The best admin panel in the world',
},
},
}
```
The Global Meta config has the same options as the [Root Metadata](#root-metadata) config.
## View Metadata
View Metadata is the metadata that is applied to specific [Views](./views) within the Admin Panel. This metadata is used to customize the title and description of a specific view, overriding any metadata set at the [Root](#root-metadata), [Collection](#collection-metadata), or [Global](#global-metadata) level.
To customize View Metadata, use the `meta` key within your View Config:
```ts
{
// ...
admin: {
views: {
dashboard: {
// highlight-start
meta: {
// highlight-end
title: 'My Dashboard',
description: 'The best dashboard in the world',
}
},
},
},
}

View File

@@ -1,244 +0,0 @@
---
title: The Admin Panel
label: Overview
order: 10
desc: Manage your data and customize the Payload Admin Panel by swapping in your own React components. Create, modify or remove views, fields, styles and much more.
keywords: admin, components, custom, customize, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Payload dynamically generates a beautiful, [fully type-safe](../typescript/overview) Admin Panel to manage your users and data. It is highly performant, even with 100+ fields, and is translated in over 30 languages. Within the Admin Panel you can manage content, [render your site](../live-preview/overview), preview drafts, [diff versions](../versions/overview), and so much more.
The Admin Panel is designed to [white-label your brand](https://payloadcms.com/blog/white-label-admin-ui). You can endlessly customize and extend the Admin UI by swapping in your own [Custom Components](./components)—everything from simple field labels to entire views can be modified or replaced to perfectly tailor the interface for your editors.
The Admin Panel is written in [TypeScript](https://www.typescriptlang.org) and built with [React](https://react.dev) using the [Next.js App Router](https://nextjs.org/docs/app). It supports [React Server Components](https://react.dev/reference/rsc/server-components), enabling the use of the [Local API](/docs/local-api/overview) on the front-end. You can install Payload into any [existing Next.js app in just one line](../getting-started/installation) and [deploy it anywhere](../production/deployment).
<Banner type="success">
The Payload Admin Panel is designed to be as minimal and straightforward as possible to allow easy customization and control. [Learn more](./components).
</Banner>
<LightDarkImage
srcLight="https://payloadcms.com/images/docs/admin.jpg"
srcDark="https://payloadcms.com/images/docs/admin-dark.jpg"
alt="Admin Panel with collapsible sidebar"
caption="Redesigned Admin Panel with a collapsible sidebar that's open by default, providing greater extensibility and enhanced horizontal real estate."
/>
## Project Structure
The Admin Panel serves as the entire HTTP layer for Payload, providing a full CRUD interface for your app. This means that both the [REST](../rest-api/overview) and [GraphQL](../graphql/overview) APIs are simply [Next.js Routes](https://nextjs.org/docs/app/building-your-application/routing) that exist directly alongside your front-end application.
Once you [install Payload](../getting-started/installation), the following files and directories will be created in your app:
```plaintext
app/
├─ (payload)/
├── admin/
├─── [[...segments]]/
├──── page.tsx
├──── not-found.tsx
├── api/
├─── [...slug]/
├──── route.ts
├── graphql/
├──── route.ts
├── graphql-playground/
├──── route.ts
├── custom.scss
├── layout.tsx
```
<Banner type="info">
If you are not familiar with Next.js project structure, you can [learn more about it here](https://nextjs.org/docs/getting-started/project-structure).
</Banner>
As shown above, all Payload routes are nested within the `(payload)` route group. This creates a boundary between the Admin Panel and the rest of your application by scoping all layouts and styles. The `layout.tsx` file within this directory, for example, is where Payload manages the `html` tag of the document to set proper [`lang`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang) and [`dir`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir) attributes, etc.
The `admin` directory contains all the _pages_ related to the interface itself, whereas the `api` and `graphql` directories contains all the _routes_ related to the [REST API](../rest-api/overview) and [GraphQL API](../graphql/overview). All admin routes are [easily configurable](#customizing-routes) to meet your application's exact requirements.
<Banner type="warning">
**Note:**
If you don't intend to use the Admin Panel, [REST API](../rest/overview), or [GraphQL API](../graphql/overview), you can opt-out by simply deleting their corresponding directories within your Next.js app. The overhead, however, is completely constrained to these routes, and will not slow down or affect Payload outside when not in use.
</Banner>
Finally, the `custom.scss` file is where you can add or override globally-oriented styles in the Admin Panel, such as modify the color palette. Customizing the look and feel through CSS alone is a powerful feature of the Admin Panel, [more on that here](./customizing-css).
All auto-generated files will contain the following comments at the top of each file:
```tsx
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */,
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
```
## Admin Options
All options for the Admin Panel are defined in your [Payload Config](../configuration/overview) under the `admin` property:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: { // highlight-line
// ...
},
})
```
The following options are available:
| Option | Description |
|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`avatar`** | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
| **`autoLogin`** | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
| **`buildPath`** | Specify an absolute path for where to store the built Admin bundle used in production. Defaults to `path.resolve(process.cwd(), 'build')`. |
| **`components`** | Component overrides that affect the entirety of the Admin Panel. [More details](./components). |
| **`custom`** | Any custom properties you wish to pass to the Admin Panel. |
| **`dateFormat`** | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). |
| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
<Banner type="success">
**Reminder:**
These are the _root-level_ options for the Admin Panel. You can also customize [Collection Admin Options](./collections) and [Global Admin Options](./globals) through their respective `admin` keys.
</Banner>
### The Admin User Collection
To specify which Collection to allow to login to the Admin Panel, pass the `admin.user` key equal to the slug of any auth-enabled Collection:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
user: 'admins', // highlight-line
},
})
```
<Banner type="warning">
**Important:**
The Admin Panel can only be used by a single auth-enabled Collection. To enable authentication for a Collection, simply set `auth: true` in the Collection's configuration. See [Authentication](../authentication/overview) for more information.
</Banner>
By default, if you have not specified a Collection, Payload will automatically provide a `User` Collection with access to the Admin Panel. You can customize or override the fields and settings of the default `User` Collection by adding your own Collection with `slug: 'users'`. Doing this will force Payload to use your provided `User` Collection instead of its default version.
You can use whatever Collection you'd like to access the Admin Panel as long as the Collection supports [Authentication](/docs/authentication/overview). It doesn't need to be called `users`. For example, you may wish to have two Collections that both support authentication:
- `admins` - meant to have a higher level of permissions to manage your data and access the Admin Panel
- `customers` - meant for end users of your app that should not be allowed to log into the Admin Panel
To do this, specify `admin: { user: 'admins' }` in your config. This will provide access to the Admin Panel to only `admins`. Any users authenticated as `customers` will be prevented from accessing the Admin Panel. See [Access Control](/docs/access-control/overview) for full details.
### Role-based Access Control
It is also possible to allow multiple user types into the Admin Panel with limited permissions, known as role-based access control (RBAC). For example, you may wish to have two roles within the `admins` Collection:
- `super-admin` - full access to the Admin Panel to perform any action
- `editor` - limited access to the Admin Panel to only manage content
To do this, add a `roles` or similar field to your auth-enabled Collection, then use the `access.admin` property to grant or deny access based on the value of that field. See [Access Control](/docs/access-control/overview) for full details. For a complete, working example of role-based access control, check out the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth).
## Customizing Routes
You have full control over the routes that Payload binds itself to. This includes both [Root-level Routes](#root-level-routes) such as the [REST API](../rest-api/overview), and [Admin-level Routes](#admin-level-routes) such as the user's account page. You can customize these routes to meet the needs of your application simply by specifying the desired paths in your config.
### Root-level Routes
Root-level routes are those that are not behind the `/admin` path, such as the [REST API](../rest-api/overview) and [GraphQL API](../graphql/overview), or the root path of the Admin Panel itself.
To customize root-level routes, use the `routes` property in your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
routes: {
admin: '/custom-admin-route' // highlight-line
}
})
```
The following options are available:
| Option | Default route | Description |
|---------------------|-----------------------|---------------------------------------------------|
| `admin` | `/admin` | The Admin Panel itself. |
| `api` | `/api` | The [REST API](../rest-api/overview) base path. |
| `graphQL` | `/graphql` | The [GraphQL API](../graphql/overview) base path. |
| `graphQLPlayground` | `/graphql-playground` | The GraphQL Playground. |
<Banner type="success">
**Tip:**
You can easily add _new_ routes to the Admin Panel through [Custom Endpoints](../rest-api/overview#custom-endpoints) and [Custom Views](./views).
</Banner>
#### Customizing Root-level Routes
You can change the Root-level Routes as needed, such as to mount the Admin Panel at the root of your application.
Changing Root-level Routes also requires a change to [Project Structure](#project-structure) to match the new route. For example, if you set `routes.admin` to `/`, you would need to completely remove the `admin` directory from the project structure:
```plaintext
app/
├─ (payload)/
├── [[...segments]]/
├──── ...
```
<Banner type="warning">
**Note:**
If you set Root-level Routes _before_ auto-generating the Admin Panel via `create-payload-app`, your [Project Structure](#project-structure) will already be set up correctly.
</Banner>
### Admin-level Routes
Admin-level routes are those behind the `/admin` path. These are the routes that are part of the Admin Panel itself, such as the user's account page, the login page, etc.
To customize admin-level routes, use the `admin.routes` property in your [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
routes: {
account: '/my-account' // highlight-line
}
},
})
```
The following options are available:
| Option | Default route | Description |
| ----------------- | ----------------------- | ----------------------------------------------- |
| `account` | `/account` | The user's account page. |
| `createFirstUser` | `/create-first-user` | The page to create the first user. |
| `forgot` | `/forgot` | The password reset page. |
| `inactivity` | `/logout-inactivity` | The page to redirect to after inactivity. |
| `login` | `/login` | The login page. |
| `logout` | `/logout` | The logout page. |
| `reset` | `/reset` | The password reset page. |
| `unauthorized` | `/unauthorized` | The unauthorized page. |
<Banner type="success">
**Note:**
You can also swap out entire _views_ out for your own, using the `admin.views` property of the Payload Config. See [Custom Views](./views) for more information.
</Banner>
## I18n
The Payload Admin Panel is translated in over [30 languages and counting](https://github.com/payloadcms/payload/tree/main/packages/translations). Languages are automatically detected based on the user's browser and used by the Admin Panel to display all text in that language. If no language was detected, or if the user's language is not yet supported, English will be chosen. Users can easily specify their language by selecting one from their account page. See [I18n](../configuration/i18n) for more information.
## Light and Dark Modes
Users in the Admin Panel have the ability to choose between light mode and dark mode for their editing experience. Users can select their preferred theme from their account page. Once selected, it is saved to their user's preferences and persisted across sessions and devices. If no theme was selected, the Admin Panel will automatically detect the operation system's theme and use that as the default.

View File

@@ -1,161 +0,0 @@
---
title: Managing User Preferences
label: Preferences
order: 70
desc: Store the preferences of your users as they interact with the Admin Panel.
keywords: admin, preferences, custom, customize, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
As your users interact with the [Admin Panel](./overview), you might want to store their preferences in a persistent manner, so that when they revisit the Admin Panel in a different session or from a different device, they can pick right back up where they left off.
Out of the box, Payload handles the persistence of your users' preferences in a handful of ways, including:
1. Columns in the Collection List View: their active state and order
1. The user's last active [Locale](../configuration/localization)
1. The "collapsed" state of `blocks`, `array`, and `collapsible` fields
1. The last-known state of the `Nav` component, etc.
<Banner type="warning">
**Important:**
All preferences are stored on an individual user basis. Payload automatically recognizes the user
that is reading or setting a preference via all provided authentication methods.
</Banner>
## Use Cases
This API is used significantly for internal operations of the Admin Panel, as mentioned above. But, if you're building your own React components for use in the Admin Panel, you can allow users to set their own preferences in correspondence to their usage of your components. For example:
- If you have built a "color picker", you could "remember" the last used colors that the user has set for easy access next time
- If you've built a custom `Nav` component, and you've built in an "accordion-style" UI, you might want to store the `collapsed` state of each Nav collapsible item. This way, if an editor returns to the panel, their `Nav` state is persisted automatically
- You might want to store `recentlyAccessed` documents to give admin editors an easy shortcut back to their recently accessed documents on the `Dashboard` or similar
- Many other use cases exist. Invent your own! Give your editors an intelligent and persistent editing experience.
## Database
Payload automatically creates an internally used `payload-preferences` Collection that stores user preferences. Each document in the `payload-preferences` Collection contains the following shape:
| Key | Value |
| ----------------- | ----------------------------------------------------------------- |
| `id` | A unique ID for each preference stored. |
| `key` | A unique `key` that corresponds to the preference. |
| `user.value` | The ID of the `user` that is storing its preference. |
| `user.relationTo` | The `slug` of the Collection that the `user` is logged in as. |
| `value` | The value of the preference. Can be any data shape that you need. |
| `createdAt` | A timestamp of when the preference was created. |
| `updatedAt` | A timestamp set to the last time the preference was updated. |
## APIs
Preferences are available to both [GraphQL](/docs/graphql/overview#preferences) and [REST](/docs/rest-api/overview#preferences) APIs.
## Adding or reading Preferences in your own components
The Payload Admin Panel offers a `usePreferences` hook. The hook is only meant for use within the Admin Panel itself. It provides you with two methods:
#### `getPreference`
This async method provides an easy way to retrieve a user's preferences by `key`. It will return a promise containing the resulting preference value.
**Arguments**
- `key`: the `key` of your preference to retrieve.
#### `setPreference`
Also async, this method provides you with an easy way to set a user preference. It returns `void`.
**Arguments:**
- `key`: the `key` of your preference to set.
- `value`: the `value` of your preference that you're looking to set.
## Example
Here is an example for how you can utilize `usePreferences` within your custom Admin Panel components. Note - this example is not fully useful and is more just a reference for how to utilize the Preferences API. In this case, we are demonstrating how to set and retrieve a user's last used colors history within a `ColorPicker` or similar type component.
```tsx
'use client'
import React, { Fragment, useState, useEffect, useCallback } from 'react';
import { usePreferences } from '@payloadcms/ui'
const lastUsedColorsPreferenceKey = 'last-used-colors';
const CustomComponent = (props) => {
const { getPreference, setPreference } = usePreferences();
// Store the last used colors in local state
const [lastUsedColors, setLastUsedColors] = useState([]);
// Callback to add a color to the last used colors
const updateLastUsedColors = useCallback((color) => {
// First, check if color already exists in last used colors.
// If it already exists, there is no need to update preferences
const colorAlreadyExists = lastUsedColors.indexOf(color) > -1;
if (!colorAlreadyExists) {
const newLastUsedColors = [
...lastUsedColors,
color,
];
setLastUsedColors(newLastUsedColors);
setPreference(lastUsedColorsPreferenceKey, newLastUsedColors);
}
}, [lastUsedColors, setPreference]);
// Retrieve preferences on component mount
// This will only be run one time, because the `getPreference` method never changes
useEffect(() => {
const asyncGetPreference = async () => {
const lastUsedColorsFromPreferences = await getPreference(lastUsedColorsPreferenceKey);
setLastUsedColors(lastUsedColorsFromPreferences);
};
asyncGetPreference();
}, [getPreference]);
return (
<div>
<button
type="button"
onClick={() => updateLastUsedColors('red')}
>
Use red
</button>
<button
type="button"
onClick={() => updateLastUsedColors('blue')}
>
Use blue
</button>
<button
type="button"
onClick={() => updateLastUsedColors('purple')}
>
Use purple
</button>
<button
type="button"
onClick={() => updateLastUsedColors('yellow')}
>
Use yellow
</button>
{lastUsedColors && (
<Fragment>
<h5>Last used colors:</h5>
<ul>
{lastUsedColors?.map((color) => (
<li key={color}>
{color}
</li>
))}
</ul>
</Fragment>
)}
</div>
);
};
export default CustomComponent;
```

View File

@@ -1,375 +0,0 @@
---
title: Customizing Views
label: Customizing Views
order: 50
desc:
keywords:
---
Views are the individual pages that make up the [Admin Panel](./overview), such as the Dashboard, List, and Edit views. One of the most powerful ways to customize the Admin Panel is to create Custom Views. These are [Custom Components](./components) that can either replace built-in views or can be entirely new.
There are four types of views within the Admin Panel:
- [Root Views](#root-views)
- [Collection Views](#collection-views)
- [Global Views](#global-views)
- [Document Views](#document-views)
To swap in your own Custom View, first consult the list of available components, determine the scope that corresponds to what you are trying to accomplish, then [author your React component(s)](#building-custom-views) accordingly.
## Root Views
Root Views are the main views of the [Admin Panel](./overview). These are views that are scoped directly under the `/admin` route, such as the Dashboard or Account views.
To swap Root Views with your own, or to [create entirely new ones](#adding-new-views), use the `admin.components.views` property of your root [Payload Config](../configuration/overview):
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
views: {
customView: {
Component: '/path/to/MyCustomView#MyCustomView', // highlight-line
path: '/my-custom-view',
}
},
},
},
})
```
Your Custom Root Views can optionally use one of the templates that Payload provides. The most common of these is the Default Template which provides the basic layout and navigation. Here is an example of what that might look like:
```tsx
import type { AdminViewProps } from 'payload'
import { DefaultTemplate } from '@payloadcms/next/templates'
import { Gutter } from '@payloadcms/ui'
import React from 'react'
export const MyCustomView: React.FC<AdminViewProps> = ({
initPageResult,
params,
searchParams,
}) => {
return (
<DefaultTemplate
i18n={initPageResult.req.i18n}
locale={initPageResult.locale}
params={params}
payload={initPageResult.req.payload}
permissions={initPageResult.permissions}
searchParams={searchParams}
user={initPageResult.req.user || undefined}
visibleEntities={initPageResult.visibleEntities}
>
<Gutter>
<h1>Custom Default Root View</h1>
<p>This view uses the Default Template.</p>
</Gutter>
</DefaultTemplate>
)
}
```
_For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._
The following options are available:
| Property | Description |
| --------------- | ----------------------------------------------------------------------------- |
| **`account`** | The Account view is used to show the currently logged in user's Account page. |
| **`dashboard`** | The main landing page of the [Admin Panel](./overview). |
For more granular control, pass a configuration object instead. Payload exposes the following properties for each view:
| Property | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`Component`** * | Pass in the component path that should be rendered when a user navigates to this route. |
| **`path`** * | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. |
| **`exact`** | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
| **`strict`** | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
| **`sensitive`** | When true, will match if the path is case sensitive.|
| **`meta`** | Page metadata overrides to apply to this view within the Admin Panel. [More details](./metadata). |
_* An asterisk denotes that a property is required._
### Adding New Views
To add a _new_ views to the [Admin Panel](./overview), simply add your own key to the `views` object with at least a `path` and `Component` property. For example:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
components: {
views: {
// highlight-start
myCustomView: {
// highlight-end
Component: '/path/to/MyCustomView#MyCustomViewComponent',
path: '/my-custom-view',
},
},
},
},
})
```
The above example shows how to add a new [Root View](#root-views), but the pattern is the same for [Collection Views](#collection-views), [Global Views](#global-views), and [Document Views](#document-views). For help on how to build your own Custom Views, see [Building Custom Views](#building-custom-views).
<Banner type="warning">
**Note:**
Routes are cascading, so unless explicitly given the `exact` property, they will
match on URLs that simply _start_ with the route's path. This is helpful when creating catch-all
routes in your application. Alternatively, define your nested route _before_ your parent
route.
</Banner>
<Banner type="warning">
**Custom views are public**
Custom views are public by default. If your view requires a user to be logged in or to have certain access rights, you should handle that within your view component yourself.
</Banner>
## Collection Views
Collection Views are views that are scoped under the `/collections` route, such as the Collection List and Document Edit views.
To swap out Collection Views with your own, or to [create entirely new ones](#adding-new-views), use the `admin.components.views` property of your [Collection Config](../collections/overview):
```ts
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
admin: {
components: {
views: {
edit: {
root: {
Component: '/path/to/MyCustomEditView', // highlight-line
}
// other options include:
// default
// versions
// version
// api
// livePreview
// [key: string]
// See "Document Views" for more details
},
list: {
Component: '/path/to/MyCustomListView',
}
},
},
},
}
```
_For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._
<Banner type="warning">
**Note:**
The `root` property will replace the _entire_ Edit View, including the title, tabs, etc., _as well as all nested [Document Views](#document-views)_, such as the API, Live Preview, and Version views. To replace only the Edit View precisely, use the `edit.default` key instead.
</Banner>
The following options are available:
| Property | Description |
| ---------- | ----------------------------------------------------------------------------------------------------------------- |
| **`edit`** | The Edit View is used to edit a single document for any given Collection. [More details](#document-views). |
| **`list`** | The List View is used to show a list of documents for any given Collection. |
<Banner type="success">
**Note:**
You can also add _new_ Collection Views to the config by adding a new key to the `views` object with at least a `path` and `Component` property. See [Adding New Views](#adding-new-views) for more information.
</Banner>
## Global Views
Global Views are views that are scoped under the `/globals` route, such as the Document Edit View.
To swap out Global Views with your own or [create entirely new ones](#adding-new-views), use the `admin.components.views` property in your [Global Config](../configuration/globals):
```ts
import type { SanitizedGlobalConfig } from 'payload'
export const MyGlobalConfig: SanitizedGlobalConfig = {
// ...
admin: {
components: {
views: {
edit: {
root: {
Component: '/path/to/MyCustomEditView', // highlight-line
}
// other options include:
// default
// versions
// version
// api
// livePreview
// [key: string]
},
},
},
},
}
```
_For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._
<Banner type="warning">
**Note:**
The `root` property will replace the _entire_ Edit View, including the title, tabs, etc., _as well as all nested [Document Views](#document-views)_, such as the API, Live Preview, and Version views. To replace only the Edit View precisely, use the `edit.default` key instead.
</Banner>
The following options are available:
| Property | Description |
| ---------- | ------------------------------------------------------------------- |
| **`edit`** | The Edit View is used to edit a single document for any given Global. [More details](#document-views). |
<Banner type="success">
**Note:**
You can also add _new_ Global Views to the config by adding a new key to the `views` object with at least a `path` and `Component` property. See [Adding New Views](#adding-new-views) for more information.
</Banner>
## Document Views
Document Views are views that are scoped under the `/collections/:collectionSlug/:id` or the `/globals/:globalSlug` route, such as the Edit View or the API View. All Document Views keep their overall structure across navigation changes, such as their title and tabs, and replace only the content below.
To swap out Document Views with your own, or to [create entirely new ones](#adding-new-views), use the `admin.components.views.Edit[key]` property in your [Collection Config](../collections/overview) or [Global Config](../configuration/globals):
```ts
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollectionOrGlobalConfig: SanitizedCollectionConfig = {
// ...
admin: {
components: {
views: {
edit: {
api: {
Component: '/path/to/MyCustomAPIViewComponent', // highlight-line
},
},
},
},
},
}
```
_For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._
<Banner type="warning">
**Note:**
If you need to replace the _entire_ Edit View, including _all_ nested Document Views, use the `root` key. See [Custom Collection Views](#collection-views) or [Custom Global Views](#global-views) for more information.
</Banner>
The following options are available:
| Property | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`root`** | The Root View overrides all other nested views and routes. No document controls or tabs are rendered when this key is set. |
| **`default`** | The Default View is the primary view in which your document is edited. It is rendered within the "Edit" tab. |
| **`versions`** | The Versions View is used to navigate the version history of a single document. It is rendered within the "Versions" tab. [More details](../versions/overview). |
| **`version`** | The Version View is used to edit a single version of a document. It is rendered within the "Version" tab. [More details](../versions/overview). |
| **`api`** | The API View is used to display the REST API JSON response for a given document. It is rendered within the "API" tab. |
| **`livePreview`** | The LivePreview view is used to display the Live Preview interface. It is rendered within the "Live Preview" tab. [More details](../live-preview/overview). |
### Document Tabs
Each Document View can be given a new tab in the Edit View, if desired. Tabs are highly configurable, from as simple as changing the label to swapping out the entire component, they can be modified in any way. To add or customize tabs in the Edit View, use the `tab` key:
```ts
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollection: SanitizedCollectionConfig = {
slug: 'my-collection',
admin: {
components: {
views: {
edit: {
myCustomTab: {
Component: '/path/to/MyCustomTab',
path: '/my-custom-tab',
tab: {
Component: '/path/to/MyCustomTabComponent' // highlight-line
}
},
anotherCustomTab: {
Component: '/path/to/AnotherCustomView',
path: '/another-custom-view',
// highlight-start
tab: {
label: 'Another Custom View',
href: '/another-custom-view',
}
// highlight-end
},
},
},
},
},
}
```
<Banner type="warning">
**Note:**
This applies to _both_ Collections _and_ Globals.
</Banner>
## Building Custom Views
Custom Views are just [Custom Components](./components) rendered at the page-level. To understand how to build Custom Views, first review the [Building Custom Components](./components#building-custom-components) guide. Once you have a Custom Component ready, you can use it as a Custom View.
```ts
import type { SanitizedCollectionConfig } from 'payload'
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
admin: {
components: {
views: {
edit: {
Component: '/path/to/MyCustomView' // highlight-line
}
},
},
},
}
```
### Default Props
Your Custom Views will be provided with the following props:
| Prop | Description |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| **`initPageResult`** | An object containing `req`, `payload`, `permissions`, etc. |
| **`clientConfig`** | The Client Config object. [More details](../admin/components#accessing-the-payload-config). |
| **`importMap`** | The import map object. |
| **`params`** | An object containing the [Dynamic Route Parameters](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes). |
| **`searchParams`** | An object containing the [Search Parameters](https://developer.mozilla.org/docs/Learn/Common_questions/What_is_a_URL#parameters). |
| **`doc`** | The document being edited. Only available in Document Views. [More details](#document-views). |
<Banner type="success">
**Reminder:**
All [Custom Server Components](./components) receive `payload` and `i18n` by default. See [Building Custom Components](./components#building-custom-components) for more details.
</Banner>
<Banner type="warning">
**Important:**
It's up to you to secure your custom views. If your view requires a user to be logged in or to
have certain access rights, you should handle that within your view component yourself.
</Banner>

View File

@@ -1,81 +0,0 @@
---
title: API Key Strategy
label: API Key Strategy
order: 50
desc: Enable API key based authentication to interface with Payload.
keywords: authentication, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
To integrate with third-party APIs or services, you might need the ability to generate API keys that can be used to identify as a certain user within Payload. API keys are generated on a user-by-user basis, similar to email and passwords, and are meant to represent a single user.
For example, if you have a third-party service or external app that needs to be able to perform protected actions against Payload, first you need to create a user within Payload, i.e. `dev@thirdparty.com`. From your external application you will need to authenticate with that user, you have two options:
1. Log in each time with that user and receive an expiring token to request with.
1. Generate a non-expiring API key for that user to request with.
<Banner type="success">
**Tip:**
This is particularly useful as you can create a "user" that reflects an integration with a specific external service and assign a "role" or specific access only needed by that service/integration.
</Banner>
Technically, both of these options will work for third-party integrations but the second option with API key is simpler, because it reduces the amount of work that your integrations need to do to be authenticated properly.
To enable API keys on a collection, set the `useAPIKey` auth option to `true`. From there, a new interface will appear in the [Admin Panel](../admin/overview) for each document within the collection that allows you to generate an API key for each user in the Collection.
```ts
import type { CollectionConfig } from 'payload'
export const ThirdPartyAccess: CollectionConfig = {
slug: 'third-party-access',
auth: {
useAPIKey: true, // highlight-line
},
fields: [],
}
```
User API keys are encrypted within the database, meaning that if your database is compromised,
your API keys will not be.
<Banner type="warning">
**Important:**
If you change your `PAYLOAD_SECRET`, you will need to regenerate your API keys.
The secret key is used to encrypt the API keys, so if you change the secret, existing API keys will
no longer be valid.
</Banner>
### HTTP Authentication
To authenticate REST or GraphQL API requests using an API key, set the `Authorization` header. The header is case-sensitive and needs the slug of the `auth.useAPIKey` enabled collection, then " API-Key ", followed by the `apiKey` that has been assigned. Payload's built-in middleware will then assign the user document to `req.user` and handle requests with the proper [Access Control](../access-control/overview). By doing this, Payload recognizes the request being made as a request by the user associated with that API key.
**For example, using Fetch:**
```ts
import Users from '../collections/Users'
const response = await fetch('http://localhost:3000/api/pages', {
headers: {
Authorization: `${Users.slug} API-Key ${YOUR_API_KEY}`,
},
})
```
Payload ensures that the same, uniform [Access Control](../access-control/overview) is used across all authentication strategies. This enables you to utilize your existing Access Control configurations with both API keys and the standard email/password authentication. This consistency can aid in maintaining granular control over your API keys.
### API Key Only Auth
If you want to use API keys as the only authentication method for a collection, you can disable the default local strategy by setting `disableLocalStrategy` to `true` on the collection's `auth` property. This will disable the ability to authenticate with email and password, and will only allow for authentication via API key.
```ts
import type { CollectionConfig } from 'payload'
export const ThirdPartyAccess: CollectionConfig = {
slug: 'third-party-access',
auth: {
useAPIKey: true,
disableLocalStrategy: true, // highlight-line
},
}
```

View File

@@ -1,131 +0,0 @@
---
title: Cookie Strategy
label: Cookie Strategy
order: 40
desc: Enable HTTP Cookie based authentication to interface with Payload.
keywords: authentication, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Payload offers the ability to [Authenticate](./overview) via HTTP-only cookies. These can be read from the responses of `login`, `logout`, `refresh`, and `me` auth operations.
<Banner type="success">
**Tip:**
You can access the logged-in user from within [Access Control](../access-control/overview) and [Hooks](../hooks/overview) through the `req.user` argument. [More details](./token-data).
</Banner>
### Automatic browser inclusion
Modern browsers automatically include `http-only` cookies when making requests directly to URLs—meaning that if you are running your API on `https://example.com`, and you have logged in and visit `https://example.com/test-page`, your browser will automatically include the Payload authentication cookie for you.
### HTTP Authentication
However, if you use `fetch` or similar APIs to retrieve Payload resources from its REST or GraphQL API, you must specify to include credentials (cookies).
Fetch example, including credentials:
```ts
const response = await fetch('http://localhost:3000/api/pages', {
credentials: 'include',
})
const pages = await response.json()
```
For more about including cookies in requests from your app to your Payload API, [read the MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Sending_a_request_with_credentials_included).
<Banner type="success">
**Tip:**
To make sure you have a Payload cookie set properly in your browser after logging in, you can use
the browsers Developer Tools > Application > Cookies > [your-domain-here]. The Developer tools
will still show HTTP-only cookies.
</Banner>
### CSRF Attacks
CSRF (cross-site request forgery) attacks are common and dangerous. By using an HTTP-only cookie, Payload removes many XSS vulnerabilities, however, CSRF attacks can still be possible.
For example, let's say you have a popular app `https://payload-finances.com` that allows users to manage finances, send and receive money. As Payload is using HTTP-only cookies, that means that browsers automatically will include cookies when sending requests to your domain - **no matter what page created the request**.
So, if a user of `https://payload-finances.com` is logged in and is browsing around on the internet, they might stumble onto a page with malicious intent. Let's look at an example:
```ts
// malicious-intent.com
// makes an authenticated request as on your behalf
const maliciousRequest = await fetch(`https://payload-finances.com/api/me`, {
credentials: 'include'
}).then(res => await res.json())
```
In this scenario, if your cookie was still valid, malicious-intent.com would be able to make requests like the one above on your behalf. This is a CSRF attack.
### 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:
```ts
// payload.config.ts
import { buildConfig } from 'payload'
const config = buildConfig({
serverURL: 'https://my-payload-instance.com',
// highlight-start
csrf: [
// whitelist of domains to allow cookie auth from
'https://your-frontend-app.com',
'https://your-other-frontend-app.com',
// `config.serverURL` is added by default if defined
],
// highlight-end
collections: [
// collections here
],
})
export default config
```
#### Cross domain authentication
If your frontend is on a different domain than your Payload API then you will not be able to use HTTP-only cookies for authentication by default as they will be considered third-party cookies by the browser.
There are a few strategies to get around this:
##### 1. Use subdomains
Cookies can cross subdomains without being considered third party cookies, for example if your API is at api.example.com then you can authenticate from example.com.
##### 2. Configure cookies
If option 1 isn't possible, then you can get around this limitation by [configuring your cookies](./overview#config-options) on your authentication collection to achieve the following setup:
```
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
```
Configuration example:
```ts
{
slug: 'users',
auth: {
cookies: {
sameSite: 'None',
secure: true,
}
},
fields: [
// your auth fields here
]
},
```
If you're configuring [cors](../production/preventing-abuse#cross-origin-resource-sharing-cors) in your Payload config, you won't be able to use a wildcard anymore, you'll need to specify the list of allowed domains.
<Banner type="success">
**Good to know:**
Setting up `secure: true` will not work if you're developing on `http://localhost` or any non-https domain. For local development you should conditionally set this to `false` based on the environment.
</Banner>

View File

@@ -1,94 +0,0 @@
---
title: Custom Strategies
label: Custom Strategies
order: 60
desc: Create custom authentication strategies to handle everything auth in Payload.
keywords: authentication, config, configuration, overview, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
<Banner type="warning">
This is an advanced feature, so only attempt this if you are an experienced developer. Otherwise,
just let Payload's built-in authentication handle user auth for you.
</Banner>
### Creating a strategy
At the core, a strategy is a way to authenticate a user making a request. As of `3.0` we moved away from [Passport](https://www.passportjs.org) in favor of pulling back the curtain and putting you in full control.
A strategy is made up of the following:
| Parameter | Description |
| --------------------------- | ------------------------------------------------------------------------- |
| **`name`** * | The name of your strategy |
| **`authenticate`** * | A function that takes in the parameters below and returns a user or null. |
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`. |
### Example Strategy
At its core a strategy simply takes information from the incoming request and returns a user. This is exactly how Payload's built-in strategies function.
Your `authenticate` method should return an object containing a Payload user document and any optional headers that you'd like Payload to set for you when we return a response.
```ts
import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = {
slug: 'users',
auth: {
disableLocalStrategy: true,
// highlight-start
strategies: [
{
name: 'custom-strategy',
authenticate: ({ payload, headers }) => {
const usersQuery = await payload.find({
collection: 'users',
where: {
code: {
equals: headers.get('code'),
},
secret: {
equals: headers.get('secret'),
},
},
})
return {
// Send the user back to authenticate,
// or send null if no user should be authenticated
user: usersQuery.docs[0] || null,
// Optionally, you can return headers
// that you'd like Payload to set here when
// it returns the response
responseHeaders: new Headers({
'some-header': 'my header value'
})
}
}
}
]
// highlight-end
},
fields: [
{
name: 'code',
type: 'text',
index: true,
unique: true,
},
{
name: 'secret',
type: 'text',
},
]
}
```

View File

@@ -1,204 +0,0 @@
---
title: Authentication Emails
label: Email Verification
order: 30
desc: Email Verification allows users to verify their email address before they're account is fully activated. Email Verification ties directly into the Email functionality that Payload provides.
keywords: authentication, email, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
[Authentication](./overview) ties directly into the [Email](../email/overview) functionality that Payload provides. This allows you to send emails to users for verification, password resets, and more. While Payload provides default email templates for these actions, you can customize them to fit your brand.
## Email Verification
Email Verification forces users to prove they have access to the email address they can authenticate. This will help to reduce spam accounts and ensure that users are who they say they are.
To enable Email Verification, use the `auth.verify` property on your [Collection Config](../configuration/collections):
```ts
import type { CollectionConfig } from 'payload'
export const Customers: CollectionConfig = {
// ...
auth: {
verify: true // highlight-line
},
}
```
<Banner type="info">
**Tip:**
Verification emails are fully customizable. [More details](#generateemailhtml).
</Banner>
The following options are available:
| Option | Description |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`generateEmailHTML`** | Allows for overriding the HTML within emails that are sent to users indicating how to validate their account. [More details](#generateemailhtml). |
| **`generateEmailSubject`** | Allows for overriding the subject of the email that is sent to users indicating how to validate their account. [More details](#generateemailsubject). |
#### generateEmailHTML
Function that accepts one argument, containing `{ req, token, user }`, that allows for overriding the HTML within emails that are sent to users indicating how to validate their account. The function should return a string that supports HTML, which can optionally be a full HTML email.
```ts
import type { CollectionConfig } from 'payload'
export const Customers: CollectionConfig = {
// ...
auth: {
verify: {
// highlight-start
generateEmailHTML: ({ req, token, user }) => {
// Use the token provided to allow your user to verify their account
const url = `https://yourfrontend.com/verify?token=${token}`
return `Hey ${user.email}, verify your email by clicking here: ${url}`
},
// highlight-end
},
},
}
```
<Banner type="warning">
**Important:**
If you specify a different URL to send your users to for email verification, such as a page on the
frontend of your app or similar, you need to handle making the call to the Payload REST or GraphQL
verification operation yourself on your frontend, using the token that was provided for you.
Above, it was passed via query parameter.
</Banner>
#### 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.
```ts
import type { CollectionConfig } from 'payload'
export const Customers: CollectionConfig = {
// ...
auth: {
verify: {
// highlight-start
generateEmailSubject: ({ req, user }) => {
return `Hey ${user.email}, reset your password!`;
}
// highlight-end
}
}
}
```
## Forgot Password
You can customize how the Forgot Password workflow operates with the following options on the `auth.forgotPassword` property:
```ts
import type { CollectionConfig } from 'payload'
export const Customers: CollectionConfig = {
// ...
auth: {
forgotPassword: { // highlight-line
// ...
},
},
}
```
The following options are available:
| Option | Description |
|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`expiration`** | Configure how long password reset tokens remain valid, specified in milliseconds. |
| **`generateEmailHTML`** | Allows for overriding the HTML within emails that are sent to users attempting to reset their password. [More details](#generateEmailHTML). |
| **`generateEmailSubject`** | Allows for overriding the subject of the email that is sent to users attempting to reset their password. [More details](#generateEmailSubject). |
#### generateEmailHTML
This function allows for overriding the HTML within emails that are sent to users attempting to reset their password. The function should return a string that supports HTML, which can be a full HTML email.
```ts
import type { CollectionConfig } from 'payload'
export const Customers: CollectionConfig = {
// ...
auth: {
forgotPassword: {
// highlight-start
generateEmailHTML: ({ req, token, user }) => {
// Use the token provided to allow your user to reset their password
const resetPasswordURL = `https://yourfrontend.com/reset-password?token=${token}`
return `
<!doctype html>
<html>
<body>
<h1>Here is my custom email template!</h1>
<p>Hello, ${user.email}!</p>
<p>Click below to reset your password.</p>
<p>
<a href="${resetPasswordURL}">${resetPasswordURL}</a>
</p>
</body>
</html>
`
},
// highlight-end
},
},
}
```
<Banner type="warning">
**Important:**
If you specify a different URL to send your users to for resetting their password, such as a page
on the frontend of your app or similar, you need to handle making the call to the Payload REST or
GraphQL reset-password operation yourself on your frontend, using the token that was provided for
you. Above, it was passed via query parameter.
</Banner>
<Banner type="success">
**Tip:**
HTML templating can be used to create custom email templates, inline CSS automatically, and more.
You can make a reusable function that standardizes all email sent from Payload, which makes
sending custom emails more DRY. Payload doesn't ship with an HTML templating engine, so you are
free to choose your own.
</Banner>
The following arguments are passed to the `generateEmailHTML` function:
| Argument | Description |
|----------|-----------------------------------------------------------------------------------------------|
| `req` | The request object. |
| `token` | The token that is generated for the user to reset their password. |
| `user` | The user document that is attempting to reset their password. |
#### 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.
```ts
import type { CollectionConfig } from 'payload'
export const Customers: CollectionConfig = {
// ...
auth: {
forgotPassword: {
// highlight-start
generateEmailSubject: ({ req, user }) => {
return `Hey ${user.email}, reset your password!`;
}
// highlight-end
}
}
}
```
The following arguments are passed to the `generateEmailSubject` function:
| Argument | Description |
|----------|-----------------------------------------------------------------------------------------------|
| `req` | The request object. |
| `user` | The user document that is attempting to reset their password. |

View File

@@ -1,51 +0,0 @@
---
title: JWT Strategy
label: JWT Strategy
order: 40
desc: Enable JSON Web Token based authentication to interface with Payload.
keywords: authentication, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
Payload offers the ability to [Authenticate](./overview) via JSON Web Tokens (JWT). These can be read from the responses of `login`, `logout`, `refresh`, and `me` auth operations.
<Banner type="success">
**Tip:**
You can access the logged-in user from within [Access Control](../access-control/overview) and [Hooks](../hooks/overview) through the `req.user` argument. [More details](./token-data).
</Banner>
### Identifying Users Via The Authorization Header
In addition to authenticating via an HTTP-only cookie, you can also identify users via the `Authorization` header on an HTTP request.
Example:
```ts
const user = await fetch('http://localhost:3000/api/users/login', {
method: 'POST',
body: JSON.stringify({
email: 'dev@payloadcms.com',
password: 'password',
})
}).then(req => await req.json())
const request = await fetch('http://localhost:3000', {
headers: {
Authorization: `JWT ${user.token}`,
},
})
```
### 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:
```ts
import type { CollectionConfig } from 'payload'
export const UsersWithoutJWTs: CollectionConfig = {
slug: 'users-without-jwts',
auth: {
removeTokenFromResponse: true, // highlight-line
},
}
```

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