Compare commits

..

40 Commits

Author SHA1 Message Date
Elliot DeNolf
0bf27b117a chore(release): v3.0.0-beta.80 [skip ci] 2024-08-14 13:14:57 -04:00
Patrik
806c22e6bd fix(next): properly closes leave-without-saving modal after navigating from Leave anyway button (#7661) 2024-08-14 13:05:26 -04:00
Alessio Gravili
39d7b717a9 fix: sidebar nav jumping around when loading page (#7574)
Fixes this:


https://github.com/user-attachments/assets/1c637bca-0c13-43f6-bcd7-6ca58da9ae77
2024-08-14 16:23:57 +00:00
Paul
9d1997e6a0 chore: update docs for redirects plugin for new redirect type feature (#7672) 2024-08-14 16:22:11 +00:00
Alessio Gravili
c65f5027d6 fix(ui): ensure field components safely access field.admin property (#7670) 2024-08-14 12:06:01 -04:00
Elliot DeNolf
dc496e4387 chore(release): v3.0.0-beta.79 [skip ci] 2024-08-14 09:21:24 -04:00
Alessio Gravili
3d86bf1974 chore: update website and blank templates to incorporate import map changes (#7664) 2024-08-14 09:10:40 -04:00
Alessio Gravili
96e7c95ebc chore: upgrade to pnpm v9, regenerate lockfile (#7369)
- regenerates the lockfile
- upgrades pnpm from v8 to v9.7.0 minimum
- ensures playwright does not import payload config. Even after our
importmap revamp that made the payload config server-only / node-safe, I
was getting these `Error: Invariant: AsyncLocalStorage accessed in
runtime where it is not available` errors in combination with pnpm v9
and lockfile regeneration.
This does not happen with pnpm v8, however I'm still blaming playwright
for this, as this does not happen in dev and we've had this specific
error with playwright in the past when we were importing the payload
config. Perhaps it's related to both playwright and the future Next.js
process importing the same config file, and not related to the config
file containing client-side React code.
Making sure playwright doesn't import the config fixed it (it was
importing it through the import map generation). The import map
generation is now run in a separate process, and playwright simply waits
for it
- One positive thing: this pr fixes a bunch of typescript errors with
react-select components. We got those errors because react-select types
are not compatible with react 19. lockfile regeneration fixed that (not
related to pnpm v9) - probably because we were installing mismatching
react versions (I saw both `fb9a90fa48-20240614` and `06d0b89e-20240801`
in our lockfile). I have thus removed the caret for react and react-dom
in our package.json - now it's consistent
2024-08-14 08:57:04 -04:00
Alessio Gravili
fca4ee995e fix(richtext-lexical): inline blocks and tables not functioning correctly if they are used in more than one editor on the same page (#7665)
Fixes https://github.com/payloadcms/payload/issues/7579

The problem was that multiple richtext editors shared the same drawer
slugs for the table and inline block drawers.
2024-08-13 21:46:23 -04:00
Elliot DeNolf
352ed0ebef ci: debug github.ref condition 2024-08-13 20:00:29 -04:00
Elliot DeNolf
bcf9b17321 ci: test github.ref 2024-08-13 19:40:37 -04:00
Alessio Gravili
a19263245f feat(richtext-lexical)!: move migration related features to /migrate subpath export in order to decrease module count when those are not used (#7660)
This lowers the module count by 31 modules

BREAKING: Migration-related lexical modules are now exported from
`@payloadcms/richtext-lexical/migrate` instead of
`@payloadcms/richtext-lexical`
2024-08-13 20:20:05 +00:00
Alessio Gravili
78e55d61be docs: move import map section from admin/overview to admin/components (#7659) 2024-08-13 19:17:14 +00:00
Alessio Gravili
cea272e189 docs: update ui field docs to use component paths (#7658) 2024-08-13 14:39:03 -04:00
Alessio Gravili
8b13dc64d1 docs: update docs with component path / client config changes (#7657) 2024-08-13 14:34:42 -04:00
Elliot DeNolf
5fc9f76406 feat: filename compound index (#7651)
Allow a compound index to be used for upload collections via a
`filenameCompoundIndex` field. Previously, `filename` was always treated
as unique.

Usage:

```ts
{
  slug: 'upload-field',
   upload: {
     // Slugs to include in compound index
     filenameCompoundIndex: ['filename', 'alt'],
  },
}
```
2024-08-13 13:55:10 -04:00
Alessio Gravili
6c0f99082b chore: install tsx in monorepo (#7656)
CI depends on it, and swc does not support the `p-limit` dependency used
in CI scripts
2024-08-13 17:32:46 +00:00
Alessio Gravili
90b7b20699 feat!: beta-next (#7620)
This PR makes three major changes to the codebase:

1. [Component Paths](#component-paths)
Instead of importing custom components into your config directly, they
are now defined as file paths and rendered only when needed. That way
the Payload config will be significantly more lightweight, and ensures
that the Payload config is 100% server-only and Node-safe. Related
discussion: https://github.com/payloadcms/payload/discussions/6938

2. [Client Config](#client-config)
Deprecates the component map by merging its logic into the client
config. The main goal of this change is for performance and
simplification. There was no need to deeply iterate over the Payload
config twice, once for the component map, and another for the client
config. Instead, we can do everything in the client config one time.
This has also dramatically simplified the client side prop drilling
through the UI library. Now, all components can share the same client
config which matches the exact shape of their Payload config (with the
exception of non-serializable props and mapped custom components).

3. [Custom client component are no longer
server-rendered](#custom-client-components-are-no-longer-server-rendered)
Previously, custom components would be server-rendered, no matter if
they are server or client components. Now, only server components are
rendered on the server. Client components are automatically detected,
and simply get passed through as `MappedComponent` to be rendered fully
client-side.

## Component Paths

Instead of importing custom components into your config directly, they
are now defined as file paths and rendered only when needed. That way
the Payload config will be significantly more lightweight, and ensures
that the Payload config is 100% server-only and Node-safe. Related
discussion: https://github.com/payloadcms/payload/discussions/6938

In order to reference any custom components in the Payload config, you
now have to specify a string path to the component instead of importing
it.

Old:

```ts
import { MyComponent2} from './MyComponent2.js'

admin: {
  components: {
    Label: MyComponent2
  },
},
```

New:

```ts
admin: {
  components: {
    Label: '/collections/Posts/MyComponent2.js#MyComponent2', // <= has to be a relative path based on a baseDir configured in the Payload config - NOT relative based on the importing file
  },
},
```

### Local API within Next.js routes

Previously, if you used the Payload Local API within Next.js pages, all
the client-side modules are being added to the bundle for that specific
page, even if you only need server-side functionality.

This `/test` route, which uses the Payload local API, was previously 460
kb. It is now down to 91 kb and does not bundle the Payload client-side
admin panel anymore.

All tests done
[here](https://github.com/payloadcms/payload-3.0-demo/tree/feat/path-test)
with beta.67/PR, db-mongodb and default richtext-lexical:

**dev /admin before:**
![CleanShot 2024-07-29 at 22 49
12@2x](https://github.com/user-attachments/assets/4428e766-b368-4bcf-8c18-d0187ab64f3e)

**dev /admin after:**
![CleanShot 2024-07-29 at 22 50
49@2x](https://github.com/user-attachments/assets/f494c848-7247-4b02-a650-a3fab4000de6)

---

**dev /test before:**
![CleanShot 2024-07-29 at 22 56
18@2x](https://github.com/user-attachments/assets/1a7e9500-b859-4761-bf63-abbcdac6f8d6)

**dev /test after:**
![CleanShot 2024-07-29 at 22 47
45@2x](https://github.com/user-attachments/assets/f89aa76d-f2d5-4572-9753-2267f034a45a)

---

**build before:**
![CleanShot 2024-07-29 at 22 57
14@2x](https://github.com/user-attachments/assets/5f8f7281-2a4a-40a5-a788-c30ddcdd51b5)

**build after::**
![CleanShot 2024-07-29 at 22 56
39@2x](https://github.com/user-attachments/assets/ea8772fd-512f-4db0-9a81-4b014715a1b7)

### Usage of the Payload Local API / config outside of Next.js

This will make it a lot easier to use the Payload config / local API in
other, server-side contexts. Previously, you might encounter errors due
to client files (like .scss files) not being allowed to be imported.

## Client Config

Deprecates the component map by merging its logic into the client
config. The main goal of this change is for performance and
simplification. There was no need to deeply iterate over the Payload
config twice, once for the component map, and another for the client
config. Instead, we can do everything in the client config one time.
This has also dramatically simplified the client side prop drilling
through the UI library. Now, all components can share the same client
config which matches the exact shape of their Payload config (with the
exception of non-serializable props and mapped custom components).

This is breaking change. The `useComponentMap` hook no longer exists,
and most component props have changed (for the better):

```ts
const { componentMap } = useComponentMap() // old
const { config } = useConfig() // new
```

The `useConfig` hook has also changed in shape, `config` is now a
property _within_ the context obj:

```ts
const config = useConfig() // old
const { config } = useConfig() // new
```

## Custom Client Components are no longer server rendered

Previously, custom components would be server-rendered, no matter if
they are server or client components. Now, only server components are
rendered on the server. Client components are automatically detected,
and simply get passed through as `MappedComponent` to be rendered fully
client-side.

The benefit of this change:

Custom client components can now receive props. Previously, the only way
for them to receive dynamic props from a parent client component was to
use hooks, e.g. `useFieldProps()`. Now, we do have the option of passing
in props to the custom components directly, if they are client
components. This will be simpler than having to look for the correct
hook.

This makes rendering them on the client a little bit more complex, as
you now have to check if that component is a server component (=>
already has been rendered) or a client component (=> not rendered yet,
has to be rendered here). However, this added complexity has been
alleviated through the easy-to-use `<RenderMappedComponent />` helper.

This helper now also handles rendering arrays of custom components (e.g.
beforeList, beforeLogin ...), which actually makes rendering custom
components easier in some cases.

## Misc improvements

This PR includes misc, breaking changes. For example, we previously
allowed unions between components and config object for the same
property. E.g. for the custom view property, you were allowed to pass in
a custom component or an object with other properties, alongside a
custom component.

Those union types are now gone. You can now either pass an object, or a
component. The previous `{ View: MyViewComponent}` is now `{ View: {
Component: MyViewComponent} }` or `{ View: { Default: { Component:
MyViewComponent} } }`.

This dramatically simplifies the way we read & process those properties,
especially in buildComponentMap. We can now simply check for the
existence of one specific property, which always has to be a component,
instead of running cursed runtime checks on a shared union property
which could contain a component, but could also contain functions or
objects.

![CleanShot 2024-07-29 at 23 07
07@2x](https://github.com/user-attachments/assets/1e75aa4c-7a4c-419f-9070-216bb7b9a5e5)

![CleanShot 2024-07-29 at 23 09
40@2x](https://github.com/user-attachments/assets/b4c96450-6b7e-496c-a4f7-59126bfd0991)

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

---------

Co-authored-by: PatrikKozak <patrik@payloadcms.com>
Co-authored-by: Paul <paul@payloadcms.com>
Co-authored-by: Paul Popus <paul@nouance.io>
Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
Co-authored-by: James <james@trbl.design>
2024-08-13 12:54:33 -04:00
Patrik
9cb84c48b9 fix(live-preview): encode query string url (#7635)
## Description

Fixes #7529 

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

## Type of change

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

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-08-13 10:24:30 -04:00
Elliot DeNolf
390f88867f chore(release): v3.0.0-beta.78 [skip ci] 2024-08-13 09:21:05 -04:00
James Mikrut
b33b5f43f4 fix: #7580 config deepmerge (#7639)
## Description

https://github.com/payloadcms/payload/issues/7580 - Fixes an infinite
loop caused by a faulty deepMerge in config sanitization.
2024-08-12 16:50:21 -04:00
Paul
56aded8507 feat: add support for custom image size file names (#7634)
Add support for custom file names in images sizes

```ts
{
  name: 'thumbnail',
  width: 400,
  height: 300,
  generateImageName: ({ height, sizeName, extension, width }) => {
    return `custom-${sizeName}-${height}-${width}.${extension}`
  },
}
```
2024-08-12 12:25:20 -06:00
Paul
78dd6a2d5b feat(plugin-form-builder): pass beforeChange params into beforeEmail hook and add types to it (#7626)
Form Builder Plugin BeforeEmail hook now takes a generic for your
generated types and it has the full hook params available to it.

```ts
import type { BeforeEmail } from '@payloadcms/plugin-form-builder'
// Your generated FormSubmission type
import type {FormSubmission} from '@payload-types'

// Pass it through and 'data' or 'originalDoc' will now be typed
const beforeEmail: BeforeEmail<FormSubmission> = (emailsToSend, beforeChangeParams) => {
  // modify the emails in any way before they are sent
  return emails.map((email) => ({
    ...email,
    html: email.html, // transform the html in any way you'd like (maybe wrap it in an html template?)
  }))
}
```
2024-08-12 12:22:52 -06:00
Alessio Gravili
a063b81460 fix: autoLogin not working if old, invalid token is present (#7456) 2024-08-12 12:41:45 -04:00
James Mikrut
18d9314f22 docs: adds prod migrations (#7631)
## Description

Adds docs for executing migrations in production.
2024-08-12 11:45:39 -04:00
Patrik
8d120373a7 fix(payload): filtering by polymorphic relationships with drafts enabled (#7570)
## Description

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

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

## Type of change

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

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
2024-08-12 10:34:21 -04:00
Patrik
f88cef5470 fix(ui): render singular label for ArrayCell when length is 1 (#7586)
## Description

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

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

## Type of change

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

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-08-12 10:33:28 -04:00
Paul
5dfcffa281 feat(plugin-redirects): added new option for redirect type in the redirects collection (#7625)
You can now add a redirect type to your redirects if needed:

```ts
// Supported types
redirectTypes: ['301', '302'],

// Override the select field
redirectTypeFieldOverride: {
  label: 'Redirect Type (Overridden)',
},
```
2024-08-11 13:18:49 -06:00
Elliot DeNolf
fa3d250053 feat: indent migration sql (#7475)
Properly indent migration sql
2024-08-09 22:41:28 -04:00
Paul
4b2a9f75d0 fix(ui): field permissions not being correctly updated when locale changes (#7611)
Closes https://github.com/payloadcms/payload/issues/7262
2024-08-09 18:39:14 -06:00
Dan Ribbens
e225783d76 chore(db-sqlite): readme header (#7609) 2024-08-09 16:45:33 -04:00
Tylan Davis
0d552fd523 chore: adjusts admin UI styling (#7557)
## Description

- Improves mobile styling of Payload admin UI.
- Reduces font size on dashboard cards.
- Improves the block/collapsible/array field styling.

- [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] Chore (non-breaking change which does not add functionality)

## Checklist:

- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
- [ ] I have made corresponding changes to the documentation
2024-08-09 11:19:36 -04:00
Paul
69ada97df5 fix(ui): apiKey field not being customisable and field access control not being updated with correct data (#7591)
You can now override the apiKey field with access control by adding this
field to your auth collection:

```ts
{
  name: 'apiKey',
  type: 'text',
  access: {
    update: ({ req }) => req.user.role === 'admin',
  }
}
```

Translated labels are now also supported.

Note that `siblingData` isn't working still in FieldAccess control and
`data` only works in non-dynamic fields, eg. fields not in an array or
block for now.
2024-08-09 08:55:17 -06:00
Paul
81e7355ee0 fix: set correct step nav path to Account on account page (#7599) 2024-08-09 00:56:11 +00:00
Paul
ce8b95f6bb fix: add editDepth to account view so that it doesn't redirect from modals (#7597)
Closes https://github.com/payloadcms/payload/issues/7593
2024-08-09 00:32:46 +00:00
James Mikrut
c1b0d93c93 feat: adds classnames to list, edit views (#7596)
## Description

Copy of #7595 for beta branch
2024-08-08 20:05:07 -04:00
Jarrod Flesch
6227276d2c fix: corrects local strategy user lookup when using loginWithUsername (#7587)
## Description

Fixes the local strategy user lookup.

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

## Type of change

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

## Checklist:

- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] Existing test suite passes locally with my changes
- [ ] I have made corresponding changes to the documentation
2024-08-08 19:51:12 -04:00
Elliot DeNolf
ee62ed6ebb chore(release): v3.0.0-beta.77 [skip ci] 2024-08-08 17:13:02 -04:00
Elliot DeNolf
0283039257 chore(db-*): remove unneeded drizzle import (#7590)
Removes unused drizzle snapshot
2024-08-08 17:11:34 -04:00
Jessica Chowdhury
b546c7b655 fix: empty path in error message (#7555)
## Description

Closes #7524

The query path is overwritten as an empty string in the
`getLocalizedPaths()` function - then when it should throw an invalid
path error it no longer has this info.

- [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

- [ ] Chore (non-breaking change which does not add functionality)
- [X] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Change to the
[templates](https://github.com/payloadcms/payload/tree/main/templates)
directory (does not affect core functionality)
- [ ] Change to the
[examples](https://github.com/payloadcms/payload/tree/main/examples)
directory (does not affect core functionality)
- [ ] This change requires a documentation update

## Checklist:

- [ ] I have added tests that prove my fix is effective or that my
feature works
- [X] Existing test suite passes locally with my changes
- [ ] I have made corresponding changes to the documentation
2024-08-08 10:29:03 +00:00
1062 changed files with 25549 additions and 27960 deletions

View File

@@ -9,7 +9,7 @@ inputs:
pnpm-version:
description: 'The pnpm version to use'
required: true
default: 8.15.7
default: 9.7.0
runs:
using: composite

View File

@@ -18,7 +18,7 @@ concurrency:
env:
NODE_VERSION: 18.20.2
PNPM_VERSION: 8.15.7
PNPM_VERSION: 9.7.0
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
@@ -427,8 +427,8 @@ jobs:
- name: Pack and build app
run: |
set -ex
pnpm run script:pack --dest templates/blank-3.0
cd templates/blank-3.0
pnpm run script:pack --dest templates/blank
cd templates/blank
cp .env.example .env
ls -la
pnpm add ./*.tgz --ignore-workspace
@@ -552,42 +552,13 @@ jobs:
publish-canary:
name: Publish Canary
if: github.ref == 'refs/heads/beta'
runs-on: ubuntu-latest
needs:
- all-green
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Load npm token
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Canary release script
# dry run hard-coded to true for testing and no npm token provided
run: pnpm tsx ./scripts/publish-canary.ts
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
# debug github.ref output
- run: |
echo github.ref: ${{ github.ref }}
echo isBeta: ${{ github.ref == 'refs/heads/beta' }}
echo isMain: ${{ github.ref == 'refs/heads/main' }}

View File

@@ -7,7 +7,7 @@ on:
env:
NODE_VERSION: 18.20.2
PNPM_VERSION: 8.15.7
PNPM_VERSION: 9.7.0
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry

6
.gitignore vendored
View File

@@ -5,6 +5,7 @@ dist
!/.idea/runConfigurations
!/.idea/payload.iml
test-results
.devcontainer
.localstack
@@ -300,3 +301,8 @@ $RECYCLE.BIN/
/build
.swc
app/(payload)/admin/importMap.js
test/live-preview/app/(payload)/admin/importMap.js
/test/live-preview/app/(payload)/admin/importMap.js
test/admin-root/app/(payload)/admin/importMap.js
/test/admin-root/app/(payload)/admin/importMap.js

View File

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

1
.idea/payload.iml generated
View File

@@ -26,6 +26,7 @@
<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" />

View File

@@ -1,8 +1,13 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev Fields" type="NodeJSConfigurationType" application-parameters="--no-deprecation fields" path-to-js-file="test/dev.js" working-dir="$PROJECT_DIR$">
<envs>
<env name="NODE_OPTIONS" value="--no-deprecation" />
</envs>
<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,8 +1,13 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev _community" type="NodeJSConfigurationType" application-parameters="--no-deprecation _community" path-to-js-file="test/dev.js" working-dir="$PROJECT_DIR$">
<envs>
<env name="NODE_OPTIONS" value="--no-deprecation" />
</envs>
<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

@@ -0,0 +1,13 @@
<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,7 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="true" type="JavaScriptTestRunnerJest">
<node-interpreter value="project" />
<node-options value="--experimental-vm-modules --no-deprecation" />
<node-options value="--no-deprecation" />
<envs />
<scope-kind value="ALL" />
<method v="2" />

3
.npmrc
View File

@@ -1,2 +1,3 @@
symlink=true
node-linker=isolated # due to a typescript bug, isolated mode requires @types/express-serve-static-core, terser and monaco-editor to be installed https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1519138189 along with two other changes in the code which I've marked with (tsbugisolatedmode) in the code
node-linker=isolated
hoist-workspace-packages=false # the default in pnpm v9 is true, but that can break our runtime dependency checks

View File

@@ -42,8 +42,8 @@
}
},
"files.insertFinalNewline": true,
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" node 'node_modules/jest/bin/jest.js'",
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--no-deprecation\" node 'node_modules/jest/bin/jest.js'",
"jestrunner.debugOptions": {
"runtimeArgs": ["--experimental-vm-modules", "--no-deprecation"]
"runtimeArgs": ["--no-deprecation"]
}
}

14
app/(app)/layout.tsx Normal file
View File

@@ -0,0 +1,14 @@
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>
)
}

11
app/(app)/test/page.tsx Normal file
View File

@@ -0,0 +1,11 @@
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

@@ -5,6 +5,8 @@ import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = {
params: {
segments: string[]
@@ -17,6 +19,7 @@ type Args = {
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, importMap, params, searchParams })
export default NotFound

View File

@@ -5,6 +5,8 @@ import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = {
params: {
segments: string[]
@@ -17,6 +19,7 @@ type Args = {
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, importMap, params, searchParams })
export default Page

View File

@@ -1,6 +1,9 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import configPromise from '@payload-config'
import { RootLayout } from '@payloadcms/next/layouts'
import { importMap } from './admin/importMap.js'
// import '@payloadcms/ui/styles.css' // Uncomment this line if `@payloadcms/ui` in `tsconfig.json` points to `/ui/dist` instead of `/ui/src`
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import React from 'react'
@@ -11,6 +14,10 @@ type Args = {
children: React.ReactNode
}
const Layout = ({ children }: Args) => <RootLayout config={configPromise}>{children}</RootLayout>
const Layout = ({ children }: Args) => (
<RootLayout config={configPromise} importMap={importMap}>
{children}
</RootLayout>
)
export default Layout

View File

@@ -51,7 +51,6 @@ To override Collection Components, use the `admin.components` property in your [
```ts
import type { SanitizedCollectionConfig } from 'payload'
import { CustomSaveButton } from './CustomSaveButton'
export const MyCollection: SanitizedCollectionConfig = {
// ...

View File

@@ -24,6 +24,178 @@ There are four main types of Custom Components in Payload:
To swap in your own Custom Component, 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 in the Payload Config
In the Payload Config, you can define custom React Components to enhance the admin interface. However, these components should not be imported directly into the server-only Payload Config to avoid including client-side code. Instead, you specify the path to the component. Heres how you can do it:
src/components/Logout.tsx
```tsx
'use client'
import React from 'react'
export const MyComponent = () => {
return (
<button>Click me!</button>
)
}
```
payload.config.ts:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: { // highlight-line
components: {
logout: {
Button: '/src/components/Logout#MyComponent'
}
}
},
})
```
In the path `/src/components/Logout#MyComponent`, `/src/components/Logout` is the file path, and `MyComponent` is the named export. If the component is the default export, the export name can be omitted. Path and export name are separated by a `#`.
### Configuring the Base Directory
Component paths, by default, are relative to your working directory - this is usually where your Next.js config lies. To simplify component paths, you have the option to configure the *base directory* using the `admin.baseDir.baseDir` property:
```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: { // highlight-line
importMap: {
baseDir: path.resolve(dirname, 'src'),
},
components: {
logout: {
Button: '/components/Logout#MyComponent'
}
}
},
})
```
In this example, we set the base directory to the `src` directory - thus we can omit the `/src/` part of our component path string.
### Passing Props
Each React Component in the Payload Config is typed as `PayloadComponent`. This usually is a string, but can also be an object containing the following properties:
| Property | Description |
|---------------|-------------------------------------------------------------------------------------------------------------------------------|
| `clientProps` | Props to be passed to the React Component if it's a Client Component |
| `exportName` | Instead of declaring named exports using `#` in the component path, you can also omit them from `path` and pass them in here. |
| `path` | Path to the React Component. Named exports can be appended to the end of the path, separated by a `#` |
| `serverProps` | Props to be passed to the React Component if it's a Server Component |
To pass in props from the config, you can use the `clientProps` and/or `serverProps` properties. This alleviates the need to use an HOC (Higher-Order-Component) to declare a React Component with props passed in.
Here is an example:
src/components/Logout.tsx
```tsx
'use client'
import React from 'react'
export const MyComponent = ({ text }: { text: string }) => {
return (
<button>Click me! {text}</button>
)
}
```
payload.config.ts:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: { // highlight-line
components: {
logout: {
Button: {
path: '/src/components/Logout',
clientProps: {
text: 'Some Text.'
},
exportName: 'MyComponent'
}
}
}
},
})
```
### Import Maps
It's essential to understand how `PayloadComponent` paths function behind the scenes. Directly importing React Components into your Payload Config using import statements can introduce client-only modules like CSS into your server-only config. This could error when attempting to load the Payload Config in server-only environments and unnecessarily increase the size of the Payload Config, which should remain streamlined and efficient for server use.
Instead, we utilize component paths to reference React Components. This method enhances the Payload Config with actual React Component imports on the client side, without affecting server-side usage. A script is deployed to scan the Payload Config, collecting all component paths and creating an `importMap.js`. This file, located in app/(payload)/admin/importMap.js, must be statically imported by your Next.js root page and layout. The script imports all the React Components from the specified paths into a Map, associating them with their respective paths (the ones you defined).
When constructing the `ClientConfig`, Payload uses the component paths as keys to fetch the corresponding React Component imports from the Import Map. It then substitutes the `PayloadComponent` with a `MappedComponent`. A `MappedComponent` includes the React Component and additional metadata, such as whether it's a server or a client component and which props it should receive. These components are then rendered through the `<RenderComponent />` component within the Payload Admin Panel.
Import maps are regenerated whenever you modify any element related to component paths. This regeneration occurs at startup and whenever Hot Module Replacement (HMR) runs. If the import maps fail to regenerate during HMR, you can restart your application and execute the `payload generate:importmap` command to manually create a new import map.
### Component paths in external packages
Component paths are resolved relative to your project's base directory, which is either your current working directory or the directory specified in `config.admin.baseDir`. When using custom components from external packages, you can't use relative paths. Instead, use an import path that's accessible as if you were writing an import statement in your project's base directory.
For example, to export a field with a custom component from an external package named `my-external-package`:
```ts
import type { Field } from 'payload'
export const MyCustomField: Field = {
type: 'text',
name: 'MyField',
admin: {
components: {
Field: 'my-external-package/client#MyFieldComponent'
}
}
}
```
Despite `MyFieldComponent` living in `src/components/MyFieldComponent.tsx` in `my-external-package`, this will not be accessible from the consuming project. Instead, we recommend exporting all custom components from one file in the external package. For example, you can define a `src/client.ts file in `my-external-package`:
```ts
'use client'
export { MyFieldComponent } from './components/MyFieldComponent'
```
Then, update the package.json of `my-external-package:
```json
{
...
"exports": {
"./client": {
"import": "./dist/client.js",
"types": "./dist/client.d.ts",
"default": "./dist/client.js"
}
}
}
```
This setup allows you to specify the component path as `my-external-package/client#MyFieldComponent` as seen above. The import map will generate:
```ts
import { MyFieldComponent } from 'my-external-package/client'
```
which is a valid way to access MyFieldComponent that can be resolved by the consuming project.
## Root Components
Root Components are those that effect the [Admin Panel](./overview) generally, such as the logo or the main nav.
@@ -33,8 +205,6 @@ To override Root Components, use the `admin.components` property in your [Payloa
```ts
import { buildConfig } from 'payload'
import { MyCustomLogo } from './MyCustomLogo'
export default buildConfig({
// ...
admin: {
@@ -81,13 +251,11 @@ To add a Custom Provider, use the `admin.components.providers` property in your
```ts
import { buildConfig } from 'payload'
import { MyProvider } from './MyProvider'
export default buildConfig({
// ...
admin: {
components: {
providers: [MyProvider], // highlight-line
providers: ['/path/to/MyProvider'], // highlight-line
},
},
})
@@ -207,7 +375,7 @@ import React from 'react'
import { useConfig } from '@payloadcms/ui'
export const MyClientComponent: React.FC = () => {
const { serverURL } = useConfig() // highlight-line
const { config: { serverURL } } = useConfig() // highlight-line
return (
<Link href={serverURL}>
@@ -221,6 +389,22 @@ export const MyClientComponent: React.FC = () => {
See [Using Hooks](#using-hooks) for more details.
</Banner>
All [Field Components](./fields) automatically receive their respective Client Field Config through a common [`field`](./fields#the-field-prop) prop:
```tsx
'use client'
import React from 'react'
import type { TextFieldProps } from 'payload'
export const MyClientFieldComponent: TextFieldProps = ({ field: { name } }) => {
return (
<p>
{`This field's name is ${name}`}
</p>
)
}
```
### 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:

View File

@@ -117,7 +117,7 @@ export const CollectionConfig: CollectionConfig = {
// ...
admin: {
components: {
Field: MyFieldComponent, // highlight-line
Field: '/path/to/MyFieldComponent', // highlight-line
},
},
}
@@ -135,32 +135,12 @@ All Field Components receive the following props:
| Property | Description |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`AfterInput`** | The rendered result of the `admin.components.afterInput` property. [More details](#afterinput-and-beforeinput). |
| **`BeforeInput`** | The rendered result of the `admin.components.beforeInput` property. [More details](#afterinput-and-beforeinput). |
| **`CustomDescription`** | The rendered result of the `admin.components.Description` property. [More details](#the-description-component). |
| **`CustomError`** | The rendered result of the `admin.components.Error` property. [More details](#the-error-component). |
| **`CustomLabel`** | The rendered result of the `admin.components.Label` property. [More details](#the-label-component).
| **`path`** | The static path of the field at render time. [More details](./hooks#usefieldprops). |
| **`disabled`** | The `admin.disabled` property defined in the [Field Config](../fields/overview). |
| **`required`** | The `admin.required` property defined in the [Field Config](../fields/overview). |
| **`className`** | The `admin.className` property defined in the [Field Config](../fields/overview). |
| **`style`** | The `admin.style` property defined in the [Field Config](../fields/overview). |
| **`custom`** | The `admin.custom` property defined in the [Field Config](../fields/overview).
| **`placeholder`** | The `admin.placeholder` property defined in the [Field Config](../fields/overview). |
| **`descriptionProps`** | An object that contains the props for the `FieldDescription` component. |
| **`labelProps`** | An object that contains the props needed for the `FieldLabel` component. |
| **`errorProps`** | An object that contains the props for the `FieldError` component. |
| **`docPreferences`** | An object that contains the preferences for the document. |
| **`label`** | The label value provided in the field, it can be used with i18n. |
| **`docPreferences`** | An object that contains the [Preferences](./preferences) for the document.
| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
| **`locale`** | The locale of the field. [More details](../configuration/localization). |
| **`localized`** | A boolean value that represents if the field is localized or not. [More details](../fields/localized). |
| **`readOnly`** | A boolean value that represents if the field is read-only or not. |
| **`rtl`** | A boolean value that represents if the field should be rendered right-to-left or not. [More details](../configuration/i18n). |
| **`user`** | The currently authenticated user. [More details](../authentication/overview). |
| **`validate`** | A function that can be used to validate the field. |
| **`hasMany`** | If a [`relationship`](../fields/relationship) field, the `hasMany` property defined in the [Field Config](../fields/overview). |
| **`maxLength`** | If a [`text`](../fields/text) field, the `maxLength` property defined in the [Field Config](../fields/overview). |
| **`minLength`** | If a [`text`](../fields/text) field, the `minLength` property defined in the [Field Config](../fields/overview). |
<Banner type="success">
<strong>Reminder:</strong>
@@ -193,6 +173,105 @@ export const CustomTextField: React.FC = () => {
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 component props to ensure type safety in your component. There is an explicit type for the Field Component, one for every [Field Type](../fields/overview). The convention is to append `Props` to the type of field, i.e. `TextFieldProps`.
```tsx
import type {
ArrayFieldProps,
BlocksFieldProps,
CheckboxFieldProps,
CodeFieldProps,
CollapsibleFieldProps,
DateFieldProps,
EmailFieldProps,
GroupFieldProps,
HiddenFieldProps,
JSONFieldProps,
NumberFieldProps,
PointFieldProps,
RadioFieldProps,
RelationshipFieldProps,
RichTextFieldProps,
RowFieldProps,
SelectFieldProps,
TabsFieldProps,
TextFieldProps,
TextareaFieldProps,
UploadFieldProps
} from 'payload'
```
### The `field` Prop
All Field Components are passed a client-friendly version of their Field Config through a common `field` prop. Since the raw Field Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types), Payload sanitized it into a [Client Config](./components#accessing-the-payload-config) that is safe to pass into Client Components.
The exact shape of this prop is unique to the specific [Field Type](../fields/overview) being rendered, minus all non-serializable properties. Any [Custom Components](../components) are also resolved into a "mapped component" that is safe to pass.
```tsx
'use client'
import React from 'react'
import type { TextFieldProps } from 'payload'
export const MyClientFieldComponent: React.FC<TextFieldProps> = ({ field: { name } }) => {
return (
<p>
{`This field's name is ${name}`}
</p>
)
}
```
The following additional properties are also provided to the `field` prop:
| Property | Description |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`_isPresentational`** | A boolean indicating that the field is purely visual and does not directly affect data or change data shape, i.e. the [UI Field](../fields/ui). |
| **`_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. `myGroup.myArray.myField` |
<Banner type="info">
<strong>Note:</strong>
These properties are underscored to denote that they are not part of the original Field Config, and instead are attached during client sanitization to make fields easier to work with on the front-end.
</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). The convention is to append `Client` to the type of field, i.e. `TextFieldClient`.
```tsx
import type {
ArrayFieldClient,
BlocksFieldClient,
CheckboxFieldClient,
CodeFieldClient,
CollapsibleFieldClient,
DateFieldClient,
EmailFieldClient,
GroupFieldClient,
HiddenFieldClient,
JSONFieldClient,
NumberFieldClient,
PointFieldClient,
RadioFieldClient,
RelationshipFieldClient,
RichTextFieldClient,
RowFieldClient,
SelectFieldClient,
TabsFieldClient,
TextFieldClient,
TextareaFieldClient,
UploadFieldClient
} from 'payload'
```
When working on the client, you will never have access to objects of type `Field`. This is reserved for the server-side. Instead, you can use `ClientField` which is a union type of all the client fields:
```tsx
import type { ClientField } from 'payload'
```
### The Cell Component
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.
@@ -207,7 +286,7 @@ export const myField: Field = {
type: 'text',
admin: {
components: {
Cell: MyCustomCell, // highlight-line
Cell: '/path/to/MyCustomCellComponent', // highlight-line
},
},
}
@@ -219,20 +298,9 @@ All Cell Components receive the following props:
| Property | Description |
| ---------------- | ----------------------------------------------------------------- |
| **`name`** | The name of the field. |
| **`className`** | The `admin.className` property defined in the [Field Config](../fields/overview). |
| **`fieldType`** | The `type` property defined in the [Field Config](../fields/overview). |
| **`schemaPath`** | The path to the field in the schema. Similar to `path`, but without dynamic indices. |
| **`isFieldAffectingData`** | A boolean value that represents if the field is affecting the data or not. |
| **`label`** | The label value provided in the field, it can be used with i18n. |
| **`labels`** | An object that contains the labels for the field. |
| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
| **`link`** | A boolean representing whether this cell should be wrapped in a link. |
| **`onClick`** | A function that is called when the cell is clicked. |
| **`dateDisplayFormat`** | If a [`date`](../fields/date) field, the `admin.dateDisplayFormat` property defined in the [Field Config](../fields/overview). |
| **`options`** | If a [`select`](../fields/select) field, this is an array of options defined in the [Field Config](../fields/overview). [More details](../fields/select). |
| **`relationTo`** | If a [`relationship`](../fields/relationship). or [`upload`](../fields/upload) field, this is the collection(s) the field is related to. |
| **`richTextComponentMap`** | If a [`richText`](../fields/rich-text) field, this is an object that maps the rich text components. [More details](../fields/rich-text). |
| **`blocks`** | If a [`blocks`](../fields/blocks) field, this is an array of labels and slugs representing the blocks defined in the [Field Config](../fields/overview). [More details](../fields/blocks). |
<Banner type="info">
<strong>Tip:</strong>
@@ -258,7 +326,7 @@ export const myField: Field = {
type: 'text',
admin: {
components: {
Label: MyCustomLabel, // highlight-line
Label: '/path/to/MyCustomLabelComponent', // highlight-line
},
},
}
@@ -270,7 +338,7 @@ Custom Label Components receive all [Field Component](#the-field-component) prop
| Property | Description |
| -------------- | ---------------------------------------------------------------- |
| **`schemaPath`** | The path to the field in the schema. Similar to `path`, but without dynamic indices. |
| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
<Banner type="success">
<strong>Reminder:</strong>
@@ -279,7 +347,7 @@ Custom Label Components receive all [Field Component](#the-field-component) prop
#### TypeScript
When building Custom Error Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Error Component, one for every [Field Type](../fields/overview). The convention is to append `ErrorComponent` to the type of field, i.e. `TextFieldErrorComponent`.
When building Custom Label Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Label Component, one for every [Field Type](../fields/overview). The convention is to append `LabelComponent` to the type of field, i.e. `TextFieldLabelComponent`.
```tsx
import type {
@@ -321,7 +389,7 @@ export const myField: Field = {
type: 'text',
admin: {
components: {
Error: MyCustomError, // highlight-line
Error: '/path/to/MyCustomErrorComponent', // highlight-line
},
},
}
@@ -333,7 +401,7 @@ Custom Error Components receive all [Field Component](#the-field-component) prop
| Property | Description |
| --------------- | ------------------------------------------------------------- |
| **`path`*** | The static path of the field at render time. [More details](./hooks#usefieldprops). |
| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
<Banner type="success">
<strong>Reminder:</strong>
@@ -443,7 +511,6 @@ To easily add a Description Component to a field, use the `admin.components.Desc
```ts
import type { SanitizedCollectionConfig } from 'payload'
import { MyCustomDescription } from './MyCustomDescription'
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
@@ -454,7 +521,7 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
type: 'text',
admin: {
components: {
Description: MyCustomDescription, // highlight-line
Description: '/path/to/MyCustomDescriptionComponent', // highlight-line
}
}
}
@@ -468,7 +535,7 @@ Custom Description Components receive all [Field Component](#the-field-component
| Property | Description |
| -------------- | ---------------------------------------------------------------- |
| **`description`** | The `description` property defined in the [Field Config](../fields/overview). |
| **`field`** | The sanitized, client-friendly version of the field's config. [More details](#the-field-prop) |
<Banner type="success">
<strong>Reminder:</strong>
@@ -524,8 +591,8 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
admin: {
components: {
// highlight-start
beforeInput: [MyCustomComponent],
afterInput: [MyOtherCustomComponent],
beforeInput: ['/path/to/MyCustomComponent'],
afterInput: ['/path/to/MyOtherCustomComponent'],
// highlight-end
}
}

View File

@@ -43,7 +43,6 @@ To override Global Components, use the `admin.components` property in your [Glob
```ts
import type { SanitizedGlobalConfig } from 'payload'
import { CustomSaveButton } from './CustomSaveButton'
export const MyGlobal: SanitizedGlobalConfig = {
// ...

View File

@@ -52,7 +52,7 @@ The `useField` hook accepts the following arguments:
The `useField` hook returns the following object:
```ts
type FieldResult<T> = {
type FieldType<T> = {
errorMessage?: string
errorPaths?: string[]
filterOptions?: FilterOptionsResult
@@ -65,7 +65,7 @@ type FieldResult<T> = {
readOnly?: boolean
rows?: Row[]
schemaPath: string
setValue: (val: unknown, disableModifyingForm?: boolean) => voi
setValue: (val: unknown, disableModifyingForm?: boolean) => void
showError: boolean
valid?: boolean
value: T
@@ -74,9 +74,9 @@ type FieldResult<T> = {
## useFieldProps
All [Custom Field Components](./fields#the-field-component) are rendered on the server, and as such, only have access to static props at render time. But, some fields can be dynamic, such as when nested in an [`array`](../fields/array) or [`blocks`](../fields/block) field. For example, items can be added, re-ordered, or deleted on-the-fly.
[Custom Field Components](./fields#the-field-component) can be rendered on the server. When using a server component as a custom field component, you can access dynamic props from within any client component rendered by your custom server component. This is done using the `useFieldProps` hook. This is important because some fields can be dynamic, such as when nested in an [`array`](../fields/array) or [`blocks`](../fields/block) field. For example, items can be added, re-ordered, or deleted on-the-fly.
For this reason, dynamic props like `path` are managed in their own React context, which can be accessed using the `useFieldProps` hook:
You can use the `useFieldProps` hooks to access dynamic props like `path`:
```tsx
'use client'
@@ -463,7 +463,7 @@ export const CustomArrayManager = () => {
name: "customArrayManager",
admin: {
components: {
Field: CustomArrayManager,
Field: '/path/to/CustomArrayManagerField',
},
},
},
@@ -560,7 +560,7 @@ export const CustomArrayManager = () => {
name: "customArrayManager",
admin: {
components: {
Field: CustomArrayManager,
Field: '/path/to/CustomArrayManagerField',
},
},
},
@@ -670,7 +670,7 @@ export const CustomArrayManager = () => {
name: "customArrayManager",
admin: {
components: {
Field: CustomArrayManager,
Field: '/path/to/CustomArrayManagerField',
},
},
},
@@ -818,7 +818,7 @@ import { useConfig } from '@payloadcms/ui'
const MyComponent: React.FC = () => {
// highlight-start
const config = useConfig()
const { config } = useConfig()
// highlight-end
return <span>{config.serverURL}</span>

View File

@@ -167,12 +167,12 @@ const config = buildConfig({
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. |
| 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">
<strong>Tip:</strong>

View File

@@ -31,7 +31,9 @@ const config = buildConfig({
admin: {
components: {
views: {
Dashboard: MyCustomDashboardView, // highlight-line
dashboard: {
Component: '/path/to/MyCustomDashboardView#MyCustomDashboardViewComponent', // highlight-line
}
},
},
},
@@ -44,8 +46,8 @@ 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). |
| **`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:
@@ -72,9 +74,9 @@ const config = buildConfig({
components: {
views: {
// highlight-start
MyCustomView: {
myCustomView: {
// highlight-end
Component: MyCustomView,
Component: '/path/to/MyCustomView#MyCustomViewComponent',
path: '/my-custom-view',
},
},
@@ -108,7 +110,9 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
admin: {
components: {
views: {
Edit: MyCustomEditView, // highlight-line
edit: {
Component: '/path/to/MyCustomEditView', // highlight-line
}
},
},
},
@@ -126,8 +130,8 @@ 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. |
| **`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">
<strong>Note:</strong>
@@ -148,7 +152,7 @@ export const MyGlobalConfig: SanitizedGlobalConfig = {
admin: {
components: {
views: {
Edit: MyCustomEditView, // highlight-line
edit: '/path/to/MyCustomEditView', // highlight-line
},
},
},
@@ -166,7 +170,7 @@ 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). |
| **`edit`** | The Edit View is used to edit a single document for any given Global. [More details](#document-views). |
<Banner type="success">
<strong>Note:</strong>
@@ -187,9 +191,9 @@ export const MyCollectionOrGlobalConfig: SanitizedCollectionConfig = {
admin: {
components: {
views: {
Edit: {
API: {
Component: MyCustomAPIView, // highlight-line
edit: {
api: {
Component: '/path/to/MyCustomAPIViewComponent', // highlight-line
},
},
},
@@ -209,15 +213,15 @@ The following options are available:
| Property | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **`Default`** | The Default view is the primary view in which your document is edited. |
| **`Versions`** | The Versions view is used to view the version history of a single document. [More details](../versions). |
| **`Version`** | The Version view is used to view a single version of a single document for a given collection. [More details](../versions). |
| **`API`** | The API view is used to display the REST API JSON response for a given document. |
| **`LivePreview`** | The LivePreview view is used to display the Live Preview interface. [More details](../live-preview). |
| **`default`** | The Default view is the primary view in which your document is edited. |
| **`versions`** | The Versions view is used to view the version history of a single document. [More details](../versions). |
| **`version`** | The Version view is used to view a single version of a single document for a given collection. [More details](../versions). |
| **`api`** | The API view is used to display the REST API JSON response for a given document. |
| **`livePreview`** | The LivePreview view is used to display the Live Preview interface. [More details](../live-preview). |
### 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 `Component.Tab` key:
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'
@@ -227,17 +231,19 @@ export const MyCollection: SanitizedCollectionConfig = {
admin: {
components: {
views: {
Edit: {
MyCustomTab: {
Component: MyCustomTab,
edit: {
myCustomTab: {
Component: '/path/to/MyCustomTab',
path: '/my-custom-tab',
Tab: MyCustomTab // highlight-line
tab: {
Component: '/path/to/MyCustomTabComponent' // highlight-line
}
},
AnotherCustomView: {
Component: AnotherCustomView,
anotherCustomTab: {
Component: '/path/to/AnotherCustomView',
path: '/another-custom-view',
// highlight-start
Tab: {
tab: {
label: 'Another Custom View',
href: '/another-custom-view',
}
@@ -261,14 +267,15 @@ Custom Views are just [Custom Components](./components) rendered at the page-lev
```ts
import type { SanitizedCollectionConfig } from 'payload'
import { MyCustomView } from './MyCustomView'
export const MyCollectionConfig: SanitizedCollectionConfig = {
// ...
admin: {
components: {
views: {
Edit: MyCustomView, // highlight-line
edit: {
Component: '/path/to/MyCustomView' // highlight-line
}
},
},
},

View File

@@ -102,5 +102,5 @@ You can import types from Payload to help make writing your Collection configs e
The `CollectionConfig` type represents a raw Collection Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedCollectionConfig` type represents a Collection Config after it has been fully sanitized. Generally, this is only used internally by Payload.
```ts
import { CollectionConfig, SanitizedCollectionConfig } from 'payload'
import type { CollectionConfig, SanitizedCollectionConfig } from 'payload'
```

View File

@@ -106,5 +106,5 @@ You can import types from Payload to help make writing your Global configs easie
The `GlobalConfig` type represents a raw Global Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedGlobalConfig` type represents a Global Config after it has been fully sanitized. Generally, this is only used internally by Payload.
```ts
import { GlobalConfig, SanitizedGlobalConfig } from 'payload'
import type { GlobalConfig, SanitizedGlobalConfig } from 'payload'
```

View File

@@ -246,5 +246,11 @@ You can import types from Payload to help make writing your config easier and ty
The `Config` type represents a raw Payload Config in its full form. Only the bare minimum properties are marked as required. The `SanitizedConfig` type represents a Payload Config after it has been fully sanitized. Generally, this is only used internally by Payload.
```ts
import { Config, SanitizedConfig } from 'payload'
import type { Config, SanitizedConfig } from 'payload'
```
## Server vs. Client
The Payload Config only lives on the server and is not allowed to contain any client-side code. That way, you can load up the Payload Config in any server environment or standalone script, without having to use Bundlers or Node.js loaders to handle importing client-only modules (e.g. scss files or React Components) without any errors.
Behind the curtains, the Next.js-based Admin Panel generates a ClientConfig, which strips away any server-only code and enriches the config with React Components.

View File

@@ -211,3 +211,32 @@ In the example above, we've specified a `ci` script which we can use as our "bui
This will require that your build pipeline can connect to your database, and it will simply run the `payload migrate` command prior to starting the build process. By calling `payload migrate`, Payload will automatically execute any migrations in your `/migrations` folder that have not yet been executed against your production database, in the order that they were created.
If it fails, the deployment will be rejected. But now, with your build script set up to run your migrations, you will be all set! Next time you deploy, your CI will execute the required migrations for you, and your database will be caught up with the shape that your Payload Config requires.
## Running migrations in production
In certain cases, you might want to run migrations at runtime when the server starts. Running them during build time may be impossible due to not having access to your database connection while building or similar reasoning.
If you're using a long-running server or container where your Node server starts up one time and then stays initialized, you might prefer to run migrations on server startup instead of within your CI.
In order to run migrations at runtime, on initialization, you can pass your migrations to your database adapter under the `prodMigrations` key as follows:
```ts
// Import your migrations from the `index.ts` file
// that Payload generates for you
import { migrations } from './migrations'
import { buildConfig } from 'payload'
export default buildConfig({
// your config here
db: postgresAdapter({
// your adapter config here
prodMigrations: migrations
})
})
```
Passing your migrations as shown above will tell Payload, in production only, to execute any migrations that need to be run prior to completing the initialization of Payload. This is ideal for long-running services where Payload will only be initialized at startup.
<Banner type="warning">
Warning - if Payload is instructed to run migrations in production, this may slow down serverless cold starts on platforms such as Vercel. Generally, this option should only be used for long-running servers / containers.
</Banner>

View File

@@ -12,7 +12,7 @@ Currently, Payload officially supports the following Database Adapters:
- [MongoDB](/docs/database/mongodb) with [Mongoose](https://mongoosejs.com/)
- [Postgres](/docs/database/postgres) with [Drizzle](https://drizzle.team/)
- Coming soon: SQLite and MySQL using Drizzle.
- [SQLite](/docs/database/sqlite) with [Drizzle](https://drizzle.team/)
To configure a Database Adapter, use the `db` property in your [Payload Config](../configuration/overview):
@@ -59,7 +59,7 @@ You should prefer MongoDB if:
Many projects might call for more rigid database architecture where the shape of your data is strongly enforced at the database level. For example, if you know the shape of your data and it's relatively "flat", and you don't anticipate it to change often, your workload might suit relational databases like Postgres very well.
You should prefer a relational DB like Postgres if:
You should prefer a relational DB like Postgres or SQLite if:
- You are comfortable with [Migrations](./migrations)
- You require enforced data consistency at the database level

View File

@@ -54,8 +54,8 @@ export const ExampleCollection: CollectionConfig = {
type: 'ui', // required
admin: {
components: {
Field: MyCustomUIField,
Cell: MyCustomUICell,
Field: '/path/to/MyCustomUIField',
Cell: '/path/to/MyCustomUICell',
},
},
},

View File

@@ -69,7 +69,7 @@ To install a Database Adapter, you can run **one** of the following commands:
#### 2. Copy Payload files into your Next.js app folder
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](https://github.com/payloadcms/payload/tree/beta/templates/blank-3.0/src/app/(payload)) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
Payload installs directly in your Next.js `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](https://github.com/payloadcms/payload/tree/beta/templates/blank/src/app/(payload)) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this:
```plaintext
app/

View File

@@ -20,7 +20,7 @@ IMPORTANT: This will overwrite all slate data. We recommend doing the following
3. Add the SlateToLexicalFeature (as seen below) first, and test it out by loading up the Admin Panel, to see if the migrator works as expected. You might have to build some custom converters for some fields first in order to convert custom Slate nodes. The SlateToLexicalFeature is where the converters are stored. Only fields with this feature added will be migrated.
```ts
import { migrateSlateToLexical } from '@payloadcms/richtext-lexical'
import { migrateSlateToLexical } from '@payloadcms/richtext-lexical/migrate'
await migrateSlateToLexical({ payload })
```
@@ -34,7 +34,8 @@ Simply add the `SlateToLexicalFeature` to your editor:
```ts
import type { CollectionConfig } from 'payload'
import { SlateToLexicalFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
import { SlateToLexicalFeature } from '@payloadcms/richtext-lexical/migrate'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
const Pages: CollectionConfig = {
slug: 'pages',
@@ -64,8 +65,8 @@ The easy way to solve this: Edit the richText field and save the document! This
If you have custom Slate nodes, create a custom converter for them. Here's the Upload converter as an example:
```ts
import type { SerializedUploadNode } from '../uploadNode.'
import type { SlateNodeConverter } from '@payloadcms/richtext-lexical'
import type { SerializedUploadNode } from '../uploadNode'
import type { SlateNodeConverter } from '@payloadcms/richtext-lexical/migrate'
export const SlateUploadConverter: SlateNodeConverter = {
converter({ slateNode }) {
@@ -95,9 +96,9 @@ When using the `SlateToLexicalFeature`, you can add your custom converters to th
```ts
import type { CollectionConfig } from 'payload'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import {
SlateToLexicalFeature,
lexicalEditor,
defaultSlateConverters,
} from '@payloadcms/richtext-lexical'

View File

@@ -2,11 +2,11 @@
title: Using Payload outside Next.js
label: Outside Next.js
order: 20
desc: Payload can be used outside of Next.js within standalone scripts or in other frameworks like Remix, Sveltekit, Nuxt, and similar.
desc: Payload can be used outside of Next.js within standalone scripts or in other frameworks like Remix, SvelteKit, Nuxt, and similar.
keywords: local api, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
---
Payload can be used completely outside of Next.js which is helpful in cases like running scripts, using Payload in a separate backend service, or using Payload's Local API to fetch your data directly from your database in other frontend frameworks like Sveltekit, Remix, Nuxt, and similar.
Payload can be used completely outside of Next.js which is helpful in cases like running scripts, using Payload in a separate backend service, or using Payload's Local API to fetch your data directly from your database in other frontend frameworks like SvelteKit, Remix, Nuxt, and similar.
<Banner>
<strong>Note:</strong>
@@ -16,38 +16,16 @@ Payload can be used completely outside of Next.js which is helpful in cases like
## Importing the Payload Config outside of Next.js
Your Payload Config likely has imports which need to be handled properly, such as CSS imports and similar. If you were to try and import your config without any Node support for SCSS / CSS files, you'll see errors that arise accordingly.
Payload provides a convenient way to run standalone scripts, which can be useful for tasks like seeding your database or performing one-off operations.
This is especially relevant if you are importing your Payload Config outside of a bundler context, such as in standalone Node scripts.
For these cases, you can use Payload's `importConfig` function to handle importing your config safely. It will handle everything you need to be able to load and use your Payload Config, without any client-side files present.
Here's an example of a seed script that creates a few documents for local development / testing purposes, using Payload's `importConfig` function to safely import Payload, and the `getPayload` function to retrieve an initialized copy of Payload.
In standalone scripts, can simply import the Payload Config and use it right away. If you need an initialized copy of Payload, you can then use the `getPayload` function. This can be useful for tasks like seeding your database or performing other one-off operations.
```ts
// We are importing `getPayload` because we don't need HMR
// for a standalone script. For usage of Payload inside Next.js,
// you should always use `import { getPayloadHMR } from '@payloadcms/next/utilities'` instead.
import { getPayload } from 'payload'
// This is a helper function that will make sure we can safely load the Payload Config
// and all of its client-side files, such as CSS, SCSS, etc.
import { importConfig } from 'payload/node'
import path from 'path'
import { fileURLToPath } from 'node:url'
import dotenv from 'dotenv'
// In ESM, you can create the "dirname" variable
// like this. We'll use this with `dotenv` to load our `.env` file, if necessary.
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
// If you don't need to load your .env file,
// then you can skip this part!
dotenv.config({
path: path.resolve(dirname, '../.env'),
})
import config from '@payload-config'
const seed = async () => {
// Get a local copy of Payload by passing your config
@@ -71,6 +49,26 @@ const seed = async () => {
}
// Call the function here to run your seed script
seed()
await seed()
```
You can then execute the script using `payload run`. Example: if you placed this standalone script in `src/seed.ts`, you would execute it like this:
```sh
payload run src/seed.ts
```
The `payload run` command does two things for you:
1. It loads the environment variables the same way Next.js loads them, eliminating the need for additional dependencies like `dotenv`. The usage of `dotenv` is not recommended, as Next.js loads environment variables differently. By using `payload run`, you ensure consistent environment variable handling across your Payload and Next.js setup.
2. It initializes swc, allowing direct execution of TypeScript files without requiring tools like tsx or ts-node.
### Troubleshooting
If you encounter import-related errors, try running the script in TSX mode:
```sh
payload run src/seed.ts --use-tsx
```
Note: Install tsx in your project first. Be aware that TSX mode is slower than the default swc mode, so only use it if necessary.

View File

@@ -62,19 +62,16 @@ If you are accessing Payload via function arguments or `req.payload`, HMR is aut
**Option 2 - outside of Next.js**
If you are using Payload outside of Next.js, for example in standalone scripts or in other frameworks, you can import Payload with no HMR functionality.
If you are using Payload outside of Next.js, for example in standalone scripts or in other frameworks, you can import Payload with no HMR functionality. Instead of using `getPayloadHMR`, you can use `getPayload`.
```ts
import { getPayload } from 'payload'
import { importConfig } from 'payload/node'
import config from '@payload-config'
const config = await importConfig('./payload.config.ts')
const payload = await getPayload({ config })
```
Both options function in exactly the same way outside of one having HMR support and the other not. However, when you import your Payload Config, you need to make sure that you can import it safely.
For more information about using Payload outside of Next.js, [click here](/docs/beta/local-api/outside-nextjs).
Both options function in exactly the same way outside of one having HMR support and the other not. For more information about using Payload outside of Next.js, [click here](/docs/beta/local-api/outside-nextjs).
## Local options available

View File

@@ -159,7 +159,6 @@ import {
useAllFormFields,
useAuth,
useClientFunctions,
useComponentMap,
useConfig,
useDebounce,
useDebouncedCallback,
@@ -212,7 +211,6 @@ import {
ActionsProvider,
AuthProvider,
ClientFunctionProvider,
ComponentMapProvider,
ConfigProvider,
DocumentEventsProvider,
DocumentInfoProvider,
@@ -299,14 +297,10 @@ import {
fieldBaseClass,
// TS Types
ActionMap,
CollectionComponentMap,
ColumnPreferences,
ConfigComponentMapBase,
DocumentInfoContext,
DocumentInfoProps,
FieldType,
FieldComponentProps,
FormProps,
RowLabelProps,
SelectFieldProps,
@@ -323,7 +317,6 @@ import {
AppHeader,
BlocksDrawer,
Column,
ComponentMap,
DefaultBlockImage,
DeleteMany,
DocumentControls,
@@ -338,7 +331,6 @@ import {
FormLoadingOverlayToggle,
FormSubmit,
GenerateConfirmation,
GlobalComponentMap,
HydrateClientUser,
ListControls,
ListSelection,
@@ -349,7 +341,8 @@ import {
PublishMany,
ReactSelect,
ReactSelectOption,
ReducedBlock,
ClientField,
ClientBlock,
RenderFields,
SectionTitle,
Select,
@@ -716,7 +709,7 @@ import type { FormState } from 'payload'
This is because the configs themselves are not serializable and so they cannot be thread through to the client, i.e. the `DocumentInfoContext`. Instead, various properties of the config are passed instead, like `collectionSlug` and `globalSlug`. You can use these to access a client-side config, if needed, through the `useConfig` hook (see next bullet).
17. The `useConfig` hook now returns a `ClientConfig` and not a `SanizitedConfig`.
17. The `useConfig` hook now returns a `ClientConfig` and not a `SanitizedConfig`.
This is because the config itself is not serializable and so it is not able to be thread through to the client, i.e. the `ConfigContext`.

View File

@@ -109,7 +109,7 @@ The `beforeEmail` property is a [beforeChange](<[beforeChange](https://payloadcm
// payload.config.ts
formBuilder({
// ...
beforeEmail: (emailsToSend) => {
beforeEmail: (emailsToSend, beforeChangeParams) => {
// modify the emails in any way before they are sent
return emails.map((email) => ({
...email,
@@ -119,6 +119,23 @@ formBuilder({
})
```
For full types with `beforeChangeParams`, you can import the types from the plugin:
```ts
import type { BeforeEmail } from '@payloadcms/plugin-form-builder'
// Your generated FormSubmission type
import type {FormSubmission} from '@payload-types'
// Pass it through and 'data' or 'originalDoc' will now be typed
const beforeEmail: BeforeEmail<FormSubmission> = (emailsToSend, beforeChangeParams) => {
// modify the emails in any way before they are sent
return emails.map((email) => ({
...email,
html: email.html, // transform the html in any way you'd like (maybe wrap it in an html template?)
}))
}
```
### `formOverrides`
Override anything on the `forms` collection by sending a [Payload Collection Config](https://payloadcms.com/docs/configuration/collections) to the `formOverrides` property.

View File

@@ -66,6 +66,8 @@ export default config
| ------------- | ---------- | ----------------------------------------------------------------------------------------------- |
| `collections` | `string[]` | An array of collection slugs to populate in the `to` field of each redirect. |
| `overrides` | `object` | A partial collection config that allows you to override anything on the `redirects` collection. |
| `redirectTypes` | `string[]` | Provide an array of redirects if you want to provide options for the type of redirects to be supported. |
| `redirectTypeFieldOverride` | `Field` | A partial Field config that allows you to override the Redirect Type field if enabled above. |
Note that the fields in overrides take a function that receives the default fields and returns an array of fields. This allows you to add fields to the collection.
@@ -83,6 +85,10 @@ redirectsPlugin({
]
},
},
redirectTypes: ['301', '302'],
redirectTypeFieldOverride: {
label: 'Redirect Type (Overridden)',
},
})
```

View File

@@ -97,6 +97,7 @@ _An asterisk denotes that an option is required._
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). |
| **`externalFileHeaderFilter`** | Accepts existing headers and returns the headers after filtering or modifying. |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
| **`filenameCompoundIndex`** | Field slugs to use for a compount index instead of the default filename index.
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the [Admin Panel](../admin/overview). The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`handlers`** | Array of Request handlers to execute when fetching a file, if a handler returns a Response it will be sent to the client. Otherwise Payload will retrieve and send back the file. |
@@ -169,6 +170,22 @@ When an uploaded image is smaller than the defined image size, we have 3 options
image size. Use the `withoutEnlargement` prop to change this.
</Banner>
#### Custom file name per size
Each image size supports a `generateImageName` function that can be used to generate a custom file name for the resized image.
This function receives the original file name, the resize name, the extension, height and width as arguments.
```ts
{
name: 'thumbnail',
width: 400,
height: 300,
generateImageName: ({ height, sizeName, extension, width }) => {
return `custom-${sizeName}-${height}-${width}.${extension}`
},
}
```
## Crop and Focal Point Selector
This feature is only available for image file types.

View File

@@ -28,6 +28,7 @@ export const rootParserOptions = {
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
EXPERIMENTAL_useProjectService: {
allowDefaultProjectForFiles: ['./src/*.ts', './src/*.tsx'],
maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 100,
},
sourceType: 'module',
ecmaVersion: 'latest',

View File

@@ -134,7 +134,7 @@ describe('Users', () => {
```json
"scripts": {
"test": "NODE_OPTIONS=--experimental-vm-modules jest --forceExit --detectOpenHandles"
"test": "jest --forceExit --detectOpenHandles"
}
```

View File

@@ -29,6 +29,6 @@
"build:server": "tsc",
"build": "yarn build:server && yarn build:payload",
"serve": "PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --forceExit --detectOpenHandles"
"test": "jest --forceExit --detectOpenHandles"
}
}

View File

@@ -1,8 +1,28 @@
const esModules = [
// file-type and all dependencies: https://github.com/sindresorhus/file-type
'file-type',
'strtok3',
'readable-web-to-node-stream',
'token-types',
'peek-readable',
'find-up',
'locate-path',
'p-locate',
'p-limit',
'yocto-queue',
'unicorn-magic',
'path-exists',
'qs-esm',
].join('|')
/** @type {import('jest').Config} */
const baseJestConfig = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
setupFiles: ['<rootDir>/test/jest.setup.env.js'],
setupFilesAfterEnv: ['<rootDir>/test/jest.setup.js'],
transformIgnorePatterns: [
`/node_modules/(?!.pnpm)(?!(${esModules})/)`,
`/node_modules/.pnpm/(?!(${esModules.replace(/\//g, '\\+')})@)`,
],
moduleNameMapper: {
'\\.(css|scss)$': '<rootDir>/test/helpers/mocks/emptyModule.js',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':

View File

@@ -1,6 +1,11 @@
import bundleAnalyzer from '@next/bundle-analyzer'
import withPayload from './packages/next/src/withPayload.js'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(__filename)
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
@@ -15,6 +20,10 @@ export default withBundleAnalyzer(
typescript: {
ignoreBuildErrors: true,
},
env: {
PAYLOAD_CORE_DEV: 'true',
ROOT_DIR: path.resolve(dirname),
},
async redirects() {
return [
{

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.76",
"version": "3.0.0-beta.80",
"private": true,
"type": "module",
"scripts": {
@@ -20,6 +20,7 @@
"build:email-nodemailer": "turbo build --filter email-nodemailer",
"build:email-resend": "turbo build --filter email-resend",
"build:eslint-config": "turbo build --filter eslint-config",
"build:essentials:force": "pnpm clean:build && turbo build --filter=\"payload...\" --filter=\"@payloadcms/ui\" --filter=\"@payloadcms/next\" --filter=\"@payloadcms/db-mongodb\" --filter=\"@payloadcms/db-postgres\" --filter=\"@payloadcms/richtext-lexical\" --filter=\"@payloadcms/translations\" --filter=\"@payloadcms/plugin-cloud\" --filter=\"@payloadcms/graphql\" --no-cache --force",
"build:force": "pnpm run build:core:force",
"build:graphql": "turbo build --filter graphql",
"build:live-preview": "turbo build --filter live-preview",
@@ -52,10 +53,12 @@
"clean:all": "node ./scripts/delete-recursively.js '@node_modules' 'media/*' '**/dist/' '**/.cache/*' '**/.next/*' '**/.turbo/*' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
"clean:build": "node ./scripts/delete-recursively.js 'media/' '**/dist/' '**/.cache/' '**/.next/' '**/.turbo/' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
"clean:cache": "node ./scripts/delete-recursively.js node_modules/.cache! packages/payload/node_modules/.cache! .next/*",
"dev": "cross-env NODE_OPTIONS=--no-deprecation node ./test/dev.js",
"dev:generate-graphql-schema": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/generateGraphQLSchema.ts",
"dev:generate-types": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/generateTypes.ts",
"dev:postgres": "cross-env NODE_OPTIONS=--no-deprecation PAYLOAD_DATABASE=postgres node ./test/dev.js",
"dev": "pnpm runts ./test/dev.ts",
"runts": "node --no-deprecation --import @swc-node/register/esm-register",
"dev:generate-graphql-schema": "pnpm runts ./test/generateGraphQLSchema.ts",
"dev:generate-importmap": "pnpm runts ./test/generateImportMap.ts",
"dev:generate-types": "pnpm runts ./test/generateTypes.ts",
"dev:postgres": "pnpm runts ./test/dev.ts",
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",
"docker:start": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml up -d",
@@ -67,20 +70,20 @@
"obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
"prepare": "husky",
"reinstall": "pnpm clean:all && pnpm install",
"release:alpha": "tsx ./scripts/release.ts --bump prerelease --tag alpha",
"release:beta": "tsx ./scripts/release.ts --bump prerelease --tag beta",
"script:gen-templates": "tsx ./scripts/generate-template-variations.ts",
"script:list-published": "tsx scripts/lib/getPackageRegistryVersions.ts",
"script:pack": "tsx scripts/pack-all-to-dest.ts",
"release:alpha": "pnpm runts ./scripts/release.ts --bump prerelease --tag alpha",
"release:beta": "pnpm runts ./scripts/release.ts --bump prerelease --tag beta",
"script:gen-templates": "pnpm runts ./scripts/generate-template-variations.ts",
"script:list-published": "pnpm runts scripts/lib/getPackageRegistryVersions.ts",
"script:pack": "pnpm runts scripts/pack-all-to-dest.ts",
"pretest": "pnpm build",
"test": "pnpm test:int && pnpm test:components && pnpm test:e2e",
"test:components": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" jest --config=jest.components.config.js",
"test:e2e": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 tsx ./test/runE2E.ts",
"test:components": "cross-env NODE_OPTIONS=\" --no-deprecation\" jest --config=jest.components.config.js",
"test:e2e": "pnpm runts ./test/runE2E.ts",
"test:e2e:debug": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 PWDEBUG=1 DISABLE_LOGGING=true playwright test",
"test:e2e:headed": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 DISABLE_LOGGING=true playwright test --headed",
"test:int": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int:postgres": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:unit": "cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
"test:int": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int:postgres": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:unit": "cross-env NODE_OPTIONS=\"--no-deprecation\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=jest.config.js --runInBand",
"translateNewKeys": "pnpm --filter payload run translateNewKeys"
},
"lint-staged": {
@@ -100,8 +103,9 @@
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
"@payloadcms/live-preview-react": "workspace:*",
"@playwright/test": "1.43.0",
"@swc/cli": "0.3.12",
"@playwright/test": "1.46.0",
"@swc-node/register": "1.10.9",
"@swc/cli": "0.4.0",
"@swc/jest": "0.2.36",
"@types/fs-extra": "^11.0.2",
"@types/jest": "29.5.12",
@@ -134,21 +138,21 @@
"next": "15.0.0-canary.104",
"open": "^10.1.0",
"p-limit": "^5.0.0",
"playwright": "1.43.0",
"playwright-core": "1.43.0",
"playwright": "1.46.0",
"playwright-core": "1.46.0",
"prettier": "3.3.2",
"prompts": "2.4.2",
"react": "^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0-rc-06d0b89e-20240801",
"react": "19.0.0-rc-06d0b89e-20240801",
"react-dom": "19.0.0-rc-06d0b89e-20240801",
"rimraf": "3.0.2",
"semver": "^7.5.4",
"sharp": "0.32.6",
"shelljs": "0.8.5",
"slash": "3.0.0",
"sort-package-json": "^2.10.0",
"swc-plugin-transform-remove-imports": "1.14.0",
"swc-plugin-transform-remove-imports": "1.15.0",
"tempy": "1.0.1",
"tsx": "4.16.2",
"tsx": "4.17.0",
"turbo": "^1.13.3",
"typescript": "5.5.4"
},
@@ -158,7 +162,7 @@
},
"engines": {
"node": "^18.20.2 || >=20.9.0",
"pnpm": "^8.15.7"
"pnpm": "^9.7.0"
},
"pnpm": {
"allowedDeprecatedVersions": {
@@ -177,9 +181,6 @@
"react": "$react",
"react-dom": "$react-dom",
"typescript": "$typescript"
},
"patchedDependencies": {
"playwright@1.43.0": "patches/playwright@1.43.0.patch"
}
},
"overrides": {

View File

@@ -22,7 +22,7 @@ const customJestConfig = {
},
testEnvironment: 'node',
testMatch: ['<rootDir>/**/*spec.ts'],
testTimeout: 90000,
testTimeout: 160000,
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest'],
},

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.76",
"version": "3.0.0-beta.80",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -42,7 +42,7 @@
"build": "pnpm pack-template-files && pnpm typecheck && pnpm build:swc",
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
"clean": "rimraf {dist,*.tsbuildinfo}",
"pack-template-files": "tsx src/scripts/pack-template-files.ts",
"pack-template-files": "node --no-deprecation --import @swc-node/register/esm-register src/scripts/pack-template-files.ts",
"prepublishOnly": "pnpm clean && pnpm build",
"test": "jest",
"typecheck": "tsc"
@@ -50,7 +50,7 @@
"dependencies": {
"@clack/prompts": "^0.7.0",
"@sindresorhus/slugify": "^1.1.0",
"@swc/core": "^1.6.13",
"@swc/core": "1.7.10",
"arg": "^5.0.0",
"chalk": "^4.1.0",
"comment-json": "^4.2.3",

View File

@@ -32,7 +32,7 @@ describe('createProject', () => {
const args = {
_: ['project-name'],
'--db': 'mongodb',
'--local-template': 'blank-3.0',
'--local-template': 'blank',
'--no-deps': true,
} as CliArgs
const packageManager = 'yarn'
@@ -64,8 +64,8 @@ describe('createProject', () => {
const templates = getValidTemplates()
it.each([
['blank-3.0', 'mongodb'],
['blank-3.0', 'postgres'],
['blank', 'mongodb'],
['blank', 'postgres'],
// TODO: Re-enable these once 3.0 is stable and templates updated
// ['website', 'mongodb'],

View File

@@ -182,7 +182,7 @@ async function installAndConfigurePayload(
const templateFilesPath =
dirname.endsWith('dist') || useDistFiles
? path.resolve(dirname, '../..', 'dist/template')
: path.resolve(dirname, '../../../../templates/blank-3.0')
: path.resolve(dirname, '../../../../templates/blank')
logDebug(`Using template files from: ${templateFilesPath}`)

View File

@@ -18,7 +18,7 @@ export function getValidTemplates(): ProjectTemplate[] {
name: 'blank',
type: 'starter',
description: 'Blank 3.0 Template',
url: 'https://github.com/payloadcms/payload/templates/blank-3.0#beta',
url: 'https://github.com/payloadcms/payload/templates/blank#beta',
},
{
name: 'website',

View File

@@ -77,7 +77,7 @@ export async function updatePayloadInProject(
const templateFilesPath =
process.env.JEST_WORKER_ID !== undefined
? path.resolve(dirname, '../../../../templates/blank-3.0')
? path.resolve(dirname, '../../../../templates/blank')
: path.resolve(dirname, '../..', 'dist/template')
const templateSrcDir = path.resolve(templateFilesPath, 'src/app/(payload)')

View File

@@ -1,6 +1,6 @@
import type { ExportDefaultExpression, ModuleItem } from '@swc/core'
import swc from '@swc/core'
import { parse } from '@swc/core'
import chalk from 'chalk'
import { Syntax, parseModule } from 'esprima-next'
import fs from 'fs'
@@ -281,10 +281,10 @@ async function compileTypeScriptFileToAST(
* https://github.com/swc-project/swc/issues/1366
*/
if (process.env.NODE_ENV === 'test') {
parseOffset = (await swc.parse('')).span.end
parseOffset = (await parse('')).span.end
}
const module = await swc.parse(fileContent, {
const module = await parse(fileContent, {
syntax: 'typescript',
})

View File

@@ -9,7 +9,7 @@ const dirname = path.dirname(filename)
main()
/**
* Copy the necessary template files from `templates/blank-3.0` to `dist/template`
* Copy the necessary template files from `templates/blank` to `dist/template`
*
* Eventually, this should be replaced with using tar.x to stream from the git repo
*/
@@ -17,7 +17,7 @@ main()
async function main() {
const root = path.resolve(dirname, '../../../../')
const outputPath = path.resolve(dirname, '../../dist/template')
const sourceTemplatePath = path.resolve(root, 'templates/blank-3.0')
const sourceTemplatePath = path.resolve(root, 'templates/blank')
if (!fs.existsSync(sourceTemplatePath)) {
throw new Error(`Source path does not exist: ${sourceTemplatePath}`)
@@ -27,7 +27,7 @@ async function main() {
fs.mkdirSync(outputPath, { recursive: true })
}
// Copy the src directory from `templates/blank-3.0` to `dist`
// Copy the src directory from `templates/blank` to `dist`
const srcPath = path.resolve(sourceTemplatePath, 'src')
const distSrcPath = path.resolve(outputPath, 'src')
// Copy entire file structure from src to dist

View File

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

View File

@@ -21,6 +21,18 @@ const buildCollectionSchema = (
},
})
if (Array.isArray(collection.upload.filenameCompoundIndex)) {
const indexDefinition: Record<string, 1> = collection.upload.filenameCompoundIndex.reduce(
(acc, index) => {
acc[index] = 1
return acc
},
{},
)
schema.index(indexDefinition, { unique: true })
}
if (config.indexSortableFields && collection.timestamps !== false) {
schema.index({ updatedAt: 1 })
schema.index({ createdAt: 1 })

View File

@@ -67,31 +67,6 @@ export type FieldGenerator<TSchema, TField> = {
schema: TSchema
}
/**
* Field config types that need representation in the database
*/
type FieldType =
| 'array'
| 'blocks'
| 'checkbox'
| 'code'
| 'collapsible'
| 'date'
| 'email'
| 'group'
| 'json'
| 'number'
| 'point'
| 'radio'
| 'relationship'
| 'richText'
| 'row'
| 'select'
| 'tabs'
| 'text'
| 'textarea'
| 'upload'
export type FieldGeneratorFunction<TSchema, TField extends Field> = (
args: FieldGenerator<TSchema, TField>,
) => void

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.76",
"version": "3.0.0-beta.80",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -42,7 +42,7 @@
"clean": "rimraf {dist,*.tsbuildinfo}",
"prepack": "pnpm clean && pnpm turbo build",
"prepublishOnly": "pnpm clean && pnpm turbo build",
"renamePredefinedMigrations": "tsx ./scripts/renamePredefinedMigrations.ts"
"renamePredefinedMigrations": "node --no-deprecation --import @swc-node/register/esm-register ./scripts/renamePredefinedMigrations.ts"
},
"dependencies": {
"@payloadcms/drizzle": "workspace:*",

View File

@@ -1,5 +1,11 @@
import type { MigrationTemplateArgs } from 'payload'
export const indent = (text: string) =>
text
.split('\n')
.map((line) => ` ${line}`)
.join('\n')
export const getMigrationTemplate = ({
downSQL,
imports,
@@ -7,10 +13,10 @@ export const getMigrationTemplate = ({
}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
${imports ? `${imports}\n` : ''}
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
${upSQL}
${indent(upSQL)}
}
export async function down({ payload, req }: MigrateDownArgs): Promise<void> {
${downSQL}
${indent(downSQL)}
}
`

View File

@@ -1,9 +1,11 @@
import type { Init, SanitizedCollectionConfig } from 'payload'
import { createTableName } from '@payloadcms/drizzle'
import { uniqueIndex } from 'drizzle-orm/pg-core'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { BaseExtraConfig } from './schema/build.js'
import type { PostgresAdapter } from './types.js'
import { buildTable } from './schema/build.js'
@@ -34,8 +36,22 @@ export const init: Init = function init(this: PostgresAdapter) {
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const baseExtraConfig: BaseExtraConfig = {}
if (collection.upload.filenameCompoundIndex) {
const indexName = `${tableName}_filename_compound_idx`
baseExtraConfig.filename_compound_index = (cols) => {
const colsConstraint = collection.upload.filenameCompoundIndex.map((f) => {
return cols[f]
})
return uniqueIndex(indexName).on(colsConstraint[0], ...colsConstraint.slice(1))
}
}
buildTable({
adapter: this,
baseExtraConfig,
disableNotNull: !!collection?.versions?.drafts,
disableUnique: false,
fields: collection.fields,

View File

@@ -38,6 +38,10 @@ export type RelationMap = Map<string, { localized: boolean; target: string; type
type Args = {
adapter: PostgresAdapter
baseColumns?: Record<string, PgColumnBuilder>
/**
* After table is created, run these functions to add extra config to the table
* ie. indexes, multiple columns, etc
*/
baseExtraConfig?: BaseExtraConfig
buildNumbers?: boolean
buildRelationships?: boolean

View File

@@ -1,4 +1,4 @@
# Payload Postgres Adapter
# Payload SQLite Adapter
Official SQLite adapter for [Payload](https://payloadcms.com).

View File

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

View File

@@ -1,5 +1,11 @@
import type { MigrationTemplateArgs } from 'payload'
export const indent = (text: string) =>
text
.split('\n')
.map((line) => ` ${line}`)
.join('\n')
export const getMigrationTemplate = ({
downSQL,
imports,
@@ -7,10 +13,10 @@ export const getMigrationTemplate = ({
}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-sqlite'
${imports ? `${imports}\n` : ''}
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
${upSQL}
${indent(upSQL)}
}
export async function down({ payload, req }: MigrateDownArgs): Promise<void> {
${downSQL}
${indent(downSQL)}
}
`

View File

@@ -1,11 +1,12 @@
/* eslint-disable no-param-reassign */
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { Init, SanitizedCollectionConfig } from 'payload'
import { createTableName } from '@payloadcms/drizzle'
import { uniqueIndex } from 'drizzle-orm/sqlite-core'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { BaseExtraConfig } from './schema/build.js'
import type { SQLiteAdapter } from './types.js'
import { buildTable } from './schema/build.js'
@@ -37,6 +38,19 @@ export const init: Init = function init(this: SQLiteAdapter) {
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const baseExtraConfig: BaseExtraConfig = {}
if (collection.upload.filenameCompoundIndex) {
const indexName = `${tableName}_filename_compound_idx`
baseExtraConfig.filename_compound_index = (cols) => {
const colsConstraint = collection.upload.filenameCompoundIndex.map((f) => {
return cols[f]
})
return uniqueIndex(indexName).on(colsConstraint[0], ...colsConstraint.slice(1))
}
}
buildTable({
adapter: this,
disableNotNull: !!collection?.versions?.drafts,

View File

@@ -40,6 +40,10 @@ export type RelationMap = Map<string, { localized: boolean; target: string; type
type Args = {
adapter: SQLiteAdapter
baseColumns?: Record<string, SQLiteColumnBuilder>
/**
* After table is created, run these functions to add extra config to the table
* ie. indexes, multiple columns, etc
*/
baseExtraConfig?: BaseExtraConfig
buildNumbers?: boolean
buildRelationships?: boolean

View File

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

View File

@@ -74,16 +74,12 @@ export const migrate: DrizzleAdapter['migrate'] = async function migrate(
}
async function runMigrationFile(payload: Payload, migration: Migration, batch: number) {
const db = payload.db as DrizzleAdapter
const { generateDrizzleJson } = db.requireDrizzleKit()
const start = Date.now()
const req = { payload } as PayloadRequest
const adapter = payload.db as DrizzleAdapter
payload.logger.info({ msg: `Migrating: ${migration.name}` })
const drizzleJSON = await generateDrizzleJson({ schema: adapter.schema })
try {
await initTransaction(req)
const db = adapter?.sessions[await req.transactionID]?.db || adapter.drizzle
@@ -94,7 +90,6 @@ async function runMigrationFile(payload: Payload, migration: Migration, batch: n
data: {
name: migration.name,
batch,
schema: drizzleJSON,
},
req,
})

View File

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

View File

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

View File

@@ -1,5 +1,21 @@
#!/usr/bin/env node
import { bin } from './dist/bin/index.js'
import { register } from 'node:module'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
bin()
// Allow disabling SWC for debugging
if (process.env.DISABLE_SWC !== 'true') {
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const url = pathToFileURL(dirname).toString() + '/'
register('@swc-node/register/esm', url)
}
const start = async () => {
const { bin } = await import('./dist/bin/index.js')
await bin()
}
void start()

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-beta.76",
"version": "3.0.0-beta.80",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -43,6 +43,7 @@
"dependencies": {
"graphql-scalars": "1.22.2",
"pluralize": "8.0.0",
"@swc-node/register": "1.10.9",
"ts-essentials": "7.0.3"
},
"devDependencies": {

View File

@@ -1,13 +1,13 @@
/* eslint-disable no-console */
import minimist from 'minimist'
import { findConfig, importConfig, loadEnv } from 'payload/node'
import { findConfig, loadEnv } from 'payload/node'
import { generateSchema } from './generateSchema.js'
export const bin = async () => {
loadEnv()
const configPath = findConfig()
const config = await importConfig(configPath)
const config = await (await import(configPath)).default
const args = minimist(process.argv.slice(2))
const script = (typeof args._[0] === 'string' ? args._[0] : '').toLowerCase()

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.0.0-beta.76",
"version": "3.0.0-beta.80",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "3.0.0-beta.76",
"version": "3.0.0-beta.80",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -71,7 +71,9 @@ export const mergeData = async <T>(args: {
try {
res = await requestHandler({
apiPath: apiRoute || '/api',
endpoint: `${collection}?depth=${depth}&where[id][in]=${Array.from(ids).join(',')}`,
endpoint: encodeURI(
`${collection}?depth=${depth}&where[id][in]=${Array.from(ids).join(',')}`,
),
serverURL,
}).then((res) => res.json())

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.0.0-beta.76",
"version": "3.0.0-beta.80",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -90,7 +90,7 @@
"esbuild": "0.23.0",
"esbuild-sass-plugin": "3.3.1",
"payload": "workspace:*",
"swc-plugin-transform-remove-imports": "1.14.0"
"swc-plugin-transform-remove-imports": "1.15.0"
},
"peerDependencies": {
"graphql": "^16.8.1",

View File

@@ -1,5 +1,6 @@
import type { DocumentTabConfig, DocumentTabProps } from 'payload'
import { RenderComponent, getCreateMappedComponent } from '@payloadcms/ui/shared'
import React, { Fragment } from 'react'
import { DocumentTabLink } from './TabLink.js'
@@ -7,22 +8,25 @@ import './index.scss'
export const baseClass = 'doc-tab'
export const DocumentTab: React.FC<DocumentTabConfig & DocumentTabProps> = (props) => {
export const DocumentTab: React.FC<
{ readonly Pill_Component?: React.FC } & DocumentTabConfig & DocumentTabProps
> = (props) => {
const {
Pill,
Pill_Component,
apiURL,
collectionConfig,
condition,
config,
globalConfig,
href: tabHref,
i18n,
isActive: tabIsActive,
label,
newTab,
payload,
permissions,
} = props
const { config } = payload
const { routes } = config
let href = typeof tabHref === 'string' ? tabHref : ''
@@ -55,6 +59,17 @@ export const DocumentTab: React.FC<DocumentTabConfig & DocumentTabProps> = (prop
})
: label
const createMappedComponent = getCreateMappedComponent({
importMap: payload.importMap,
serverProps: {
i18n,
payload,
permissions,
},
})
const mappedPin = createMappedComponent(Pill, undefined, Pill_Component, 'Pill')
return (
<DocumentTabLink
adminRoute={routes.admin}
@@ -67,10 +82,10 @@ export const DocumentTab: React.FC<DocumentTabConfig & DocumentTabProps> = (prop
>
<span className={`${baseClass}__label`}>
{labelToRender}
{Pill && (
{mappedPin && (
<Fragment>
&nbsp;
<Pill />
<RenderComponent mappedComponent={mappedPin} />
</Fragment>
)}
</span>

View File

@@ -12,9 +12,9 @@ export const getCustomViews = (args: {
if (collectionConfig) {
const collectionViewsConfig =
typeof collectionConfig?.admin?.components?.views?.Edit === 'object' &&
typeof collectionConfig?.admin?.components?.views?.Edit !== 'function'
? collectionConfig?.admin?.components?.views?.Edit
typeof collectionConfig?.admin?.components?.views?.edit === 'object' &&
typeof collectionConfig?.admin?.components?.views?.edit !== 'function'
? collectionConfig?.admin?.components?.views?.edit
: undefined
customViews = Object.entries(collectionViewsConfig || {}).reduce((prev, [key, view]) => {
@@ -28,9 +28,9 @@ export const getCustomViews = (args: {
if (globalConfig) {
const globalViewsConfig =
typeof globalConfig?.admin?.components?.views?.Edit === 'object' &&
typeof globalConfig?.admin?.components?.views?.Edit !== 'function'
? globalConfig?.admin?.components?.views?.Edit
typeof globalConfig?.admin?.components?.views?.edit === 'object' &&
typeof globalConfig?.admin?.components?.views?.edit !== 'function'
? globalConfig?.admin?.components?.views?.edit
: undefined
customViews = Object.entries(globalViewsConfig || {}).reduce((prev, [key, view]) => {

View File

@@ -9,9 +9,9 @@ export const getViewConfig = (args: {
if (collectionConfig) {
const collectionConfigViewsConfig =
typeof collectionConfig?.admin?.components?.views?.Edit === 'object' &&
typeof collectionConfig?.admin?.components?.views?.Edit !== 'function'
? collectionConfig?.admin?.components?.views?.Edit
typeof collectionConfig?.admin?.components?.views?.edit === 'object' &&
typeof collectionConfig?.admin?.components?.views?.edit !== 'function'
? collectionConfig?.admin?.components?.views?.edit
: undefined
return collectionConfigViewsConfig?.[name]
@@ -19,9 +19,9 @@ export const getViewConfig = (args: {
if (globalConfig) {
const globalConfigViewsConfig =
typeof globalConfig?.admin?.components?.views?.Edit === 'object' &&
typeof globalConfig?.admin?.components?.views?.Edit !== 'function'
? globalConfig?.admin?.components?.views?.Edit
typeof globalConfig?.admin?.components?.views?.edit === 'object' &&
typeof globalConfig?.admin?.components?.views?.edit !== 'function'
? globalConfig?.admin?.components?.views?.edit
: undefined
return globalConfigViewsConfig?.[name]

View File

@@ -37,7 +37,6 @@
&__tabs {
padding: 0;
margin-left: var(--gutter-h);
}
}

View File

@@ -1,11 +1,12 @@
import type { I18n } from '@payloadcms/translations'
import type {
Payload,
Permissions,
SanitizedCollectionConfig,
SanitizedConfig,
SanitizedGlobalConfig,
} from 'payload'
import { RenderComponent, getCreateMappedComponent } from '@payloadcms/ui/shared'
import { isPlainObject } from 'payload'
import React from 'react'
@@ -20,12 +21,13 @@ const baseClass = 'doc-tabs'
export const DocumentTabs: React.FC<{
collectionConfig: SanitizedCollectionConfig
config: SanitizedConfig
globalConfig: SanitizedGlobalConfig
i18n: I18n
payload: Payload
permissions: Permissions
}> = (props) => {
const { collectionConfig, config, globalConfig, permissions } = props
const { collectionConfig, globalConfig, i18n, payload, permissions } = props
const { config } = payload
const customViews = getCustomViews({ collectionConfig, globalConfig })
@@ -46,10 +48,9 @@ export const DocumentTabs: React.FC<{
})
?.map(([name, tab], index) => {
const viewConfig = getViewConfig({ name, collectionConfig, globalConfig })
const tabFromConfig = viewConfig && 'Tab' in viewConfig ? viewConfig.Tab : undefined
const tabConfig = typeof tabFromConfig === 'object' ? tabFromConfig : undefined
const tabFromConfig = viewConfig && 'tab' in viewConfig ? viewConfig.tab : undefined
const { condition } = tabConfig || {}
const { condition } = tabFromConfig || {}
const meetsCondition =
!condition ||
@@ -72,17 +73,39 @@ export const DocumentTabs: React.FC<{
return null
})}
{customViews?.map((CustomView, index) => {
if ('Tab' in CustomView) {
const { Tab, path } = CustomView
if ('tab' in CustomView) {
const { path, tab } = CustomView
if (typeof Tab === 'object' && !isPlainObject(Tab)) {
throw new Error(
`Custom 'Tab' Component for path: "${path}" must be a React Server Component. To use client-side functionality, render your Client Component within a Server Component and pass it only props that are serializable. More info: https://react.dev/reference/react/use-server#serializable-parameters-and-return-values`,
if (tab.Component) {
const createMappedComponent = getCreateMappedComponent({
importMap: payload.importMap,
serverProps: {
i18n,
payload,
permissions,
...props,
key: `tab-custom-${index}`,
path,
},
})
const mappedTab = createMappedComponent(
tab.Component,
undefined,
undefined,
'tab.Component',
)
}
if (typeof Tab === 'function') {
return <Tab path={path} {...props} key={`tab-custom-${index}`} />
return (
<RenderComponent
clientProps={{
key: `tab-custom-${index}`,
path,
}}
key={`tab-custom-${index}`}
mappedComponent={mappedTab}
/>
)
}
return (
@@ -90,7 +113,7 @@ export const DocumentTabs: React.FC<{
key={`tab-custom-${index}`}
{...{
...props,
...Tab,
...tab,
}}
/>
)

View File

@@ -1,15 +1,16 @@
import type { DocumentTabConfig } from 'payload'
import type React from 'react'
import { VersionsPill } from './VersionsPill/index.js'
export const documentViewKeys = [
'API',
'Default',
'LivePreview',
'References',
'Relationships',
'Version',
'Versions',
'api',
'default',
'livePreview',
'references',
'relationships',
'version',
'versions',
]
export type DocumentViewKey = (typeof documentViewKeys)[number]
@@ -17,10 +18,11 @@ export type DocumentViewKey = (typeof documentViewKeys)[number]
export const tabs: Record<
DocumentViewKey,
{
Pill_Component?: React.FC
order?: number // TODO: expose this to the globalConfig config
} & DocumentTabConfig
> = {
API: {
api: {
condition: ({ collectionConfig, globalConfig }) =>
(collectionConfig && !collectionConfig?.admin?.hideAPIURL) ||
(globalConfig && !globalConfig?.admin?.hideAPIURL),
@@ -28,14 +30,14 @@ export const tabs: Record<
label: 'API',
order: 1000,
},
Default: {
default: {
href: '',
// isActive: ({ href, location }) =>
// location.pathname === href || location.pathname === `${href}/create`,
label: ({ t }) => t('general:edit'),
order: 0,
},
LivePreview: {
livePreview: {
condition: ({ collectionConfig, config, globalConfig }) => {
if (collectionConfig) {
return Boolean(
@@ -57,17 +59,17 @@ export const tabs: Record<
label: ({ t }) => t('general:livePreview'),
order: 100,
},
References: {
references: {
condition: () => false,
},
Relationships: {
relationships: {
condition: () => false,
},
Version: {
version: {
condition: () => false,
},
Versions: {
Pill: VersionsPill,
versions: {
Pill_Component: VersionsPill,
condition: ({ collectionConfig, globalConfig, permissions }) =>
Boolean(
(collectionConfig?.versions &&

View File

@@ -40,8 +40,6 @@
&__title {
width: 100%;
padding-left: var(--gutter-h);
padding-right: var(--gutter-h);
}
}

View File

@@ -1,8 +1,8 @@
import type { I18n } from '@payloadcms/translations'
import type {
Payload,
Permissions,
SanitizedCollectionConfig,
SanitizedConfig,
SanitizedGlobalConfig,
} from 'payload'
@@ -16,14 +16,14 @@ const baseClass = `doc-header`
export const DocumentHeader: React.FC<{
collectionConfig?: SanitizedCollectionConfig
config: SanitizedConfig
customHeader?: React.ReactNode
globalConfig?: SanitizedGlobalConfig
hideTabs?: boolean
i18n: I18n
payload: Payload
permissions: Permissions
}> = (props) => {
const { collectionConfig, config, customHeader, globalConfig, hideTabs, i18n, permissions } =
const { collectionConfig, customHeader, globalConfig, hideTabs, i18n, payload, permissions } =
props
return (
@@ -35,9 +35,9 @@ export const DocumentHeader: React.FC<{
{!hideTabs && (
<DocumentTabs
collectionConfig={collectionConfig}
config={config}
globalConfig={globalConfig}
i18n={i18n}
payload={payload}
permissions={permissions}
/>
)}

View File

@@ -7,7 +7,7 @@ import { email, username } from 'payload/shared'
import React from 'react'
type Props = {
loginWithUsername?: LoginWithUsernameOptions | false
readonly loginWithUsername?: LoginWithUsernameOptions | false
}
function EmailFieldComponent(props: Props) {
const { loginWithUsername } = props
@@ -21,10 +21,11 @@ function EmailFieldComponent(props: Props) {
return (
<EmailField
autoComplete="off"
label={t('general:email')}
name="email"
path="email"
required={requireEmail}
field={{
name: 'email',
label: t('general:email'),
required: requireEmail,
}}
validate={email}
/>
)
@@ -43,10 +44,11 @@ function UsernameFieldComponent(props: Props) {
if (showUsernameField) {
return (
<TextField
label={t('authentication:username')}
name="username"
path="username"
required={requireUsername}
field={{
name: 'username',
label: t('authentication:username'),
required: requireUsername,
}}
validate={username}
/>
)
@@ -70,25 +72,34 @@ export function RenderEmailAndUsernameFields(props: RenderEmailAndUsernameFields
return (
<RenderFields
className={className}
fieldMap={[
fields={[
{
name: 'email',
type: 'text',
CustomField: <EmailFieldComponent loginWithUsername={loginWithUsername} />,
cellComponentProps: null,
fieldComponentProps: { type: 'email', autoComplete: 'off', readOnly },
fieldIsPresentational: false,
isFieldAffectingData: true,
admin: {
autoComplete: 'off',
components: {
Field: {
type: 'client',
Component: null,
RenderedComponent: <EmailFieldComponent loginWithUsername={loginWithUsername} />,
},
},
},
localized: false,
},
{
name: 'username',
type: 'text',
CustomField: <UsernameFieldComponent loginWithUsername={loginWithUsername} />,
cellComponentProps: null,
fieldComponentProps: { type: 'text', readOnly },
fieldIsPresentational: false,
isFieldAffectingData: true,
admin: {
components: {
Field: {
type: 'client',
Component: null,
RenderedComponent: <UsernameFieldComponent loginWithUsername={loginWithUsername} />,
},
},
},
localized: false,
},
]}

View File

@@ -13,6 +13,7 @@
display: flex;
flex-direction: column;
gap: var(--base);
padding: base(2);
}
&__content {

View File

@@ -50,6 +50,7 @@ const Component: React.FC<{
}
export const LeaveWithoutSaving: React.FC = () => {
const { closeModal } = useModal()
const modified = useFormModified()
const { user } = useAuth()
const [show, setShow] = React.useState(false)
@@ -61,7 +62,11 @@ export const LeaveWithoutSaving: React.FC = () => {
setShow(true)
}, [])
usePreventLeave({ hasAccepted, onPrevent, prevent })
const handleAccept = useCallback(() => {
closeModal(modalSlug)
}, [closeModal])
usePreventLeave({ hasAccepted, onAccept: handleAccept, onPrevent, prevent })
return (
<Component

View File

@@ -57,12 +57,14 @@ export const useBeforeUnload = (enabled: (() => boolean) | boolean = true, messa
export const usePreventLeave = ({
hasAccepted = false,
message = 'Are you sure want to leave this page?',
onAccept,
onPrevent,
prevent = true,
}: {
hasAccepted: boolean
// if no `onPrevent` is provided, the message will be displayed in a confirm dialog
message?: string
onAccept?: () => void
// to use a custom confirmation dialog, provide a function that returns a boolean
onPrevent?: () => void
prevent: boolean
@@ -142,7 +144,8 @@ export const usePreventLeave = ({
useEffect(() => {
if (hasAccepted && cancelledURL.current) {
if (onAccept) onAccept()
router.push(cancelledURL.current)
}
}, [hasAccepted, router])
}, [hasAccepted, onAccept, router])
}

View File

@@ -1,6 +1,6 @@
import type { ServerProps } from 'payload'
import { PayloadLogo, RenderCustomComponent } from '@payloadcms/ui/shared'
import { PayloadLogo, RenderComponent, getCreateMappedComponent } from '@payloadcms/ui/shared'
import React from 'react'
export const Logo: React.FC<ServerProps> = (props) => {
@@ -16,19 +16,20 @@ export const Logo: React.FC<ServerProps> = (props) => {
} = {},
} = payload.config
return (
<RenderCustomComponent
CustomComponent={CustomLogo}
DefaultComponent={PayloadLogo}
serverOnlyProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
}}
/>
)
const createMappedComponent = getCreateMappedComponent({
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
},
})
const mappedCustomLogo = createMappedComponent(CustomLogo, undefined, PayloadLogo, 'CustomLogo')
return <RenderComponent mappedComponent={mappedCustomLogo} />
}

View File

@@ -9,7 +9,10 @@
width: var(--nav-width);
border-right: 1px solid var(--theme-elevation-100);
opacity: 0;
transition: opacity var(--nav-trans-time) ease-in-out;
&--nav-animate {
transition: opacity var(--nav-trans-time) ease-in-out;
}
&--nav-open {
opacity: 1;

View File

@@ -10,10 +10,19 @@ export const NavWrapper: React.FC<{
}> = (props) => {
const { baseClass, children } = props
const { navOpen, navRef } = useNav()
const { hydrated, navOpen, navRef, shouldAnimate } = useNav()
return (
<aside className={[baseClass, navOpen && `${baseClass}--nav-open`].filter(Boolean).join(' ')}>
<aside
className={[
baseClass,
navOpen && `${baseClass}--nav-open`,
shouldAnimate && `${baseClass}--nav-animate`,
hydrated && `${baseClass}--nav-hydrated`,
]
.filter(Boolean)
.join(' ')}
>
<div className={`${baseClass}__scroll`} ref={navRef}>
{children}
</div>

View File

@@ -25,9 +25,11 @@ export const DefaultNavClient: React.FC = () => {
const pathname = usePathname()
const {
collections,
globals,
routes: { admin: adminRoute },
config: {
collections,
globals,
routes: { admin: adminRoute },
},
} = useConfig()
const { i18n } = useTranslation()
@@ -98,11 +100,7 @@ export const DefaultNavClient: React.FC = () => {
key={i}
tabIndex={!navOpen ? -1 : undefined}
>
{activeCollection && (
<span className={`${baseClass}__link-icon`}>
<ChevronIcon direction="right" />
</span>
)}
{activeCollection && <div className={`${baseClass}__link-indicator`} />}
<span className={`${baseClass}__link-label`}>{entityLabel}</span>
</LinkElement>
)

View File

@@ -9,7 +9,11 @@
width: var(--nav-width);
border-right: 1px solid var(--theme-elevation-100);
opacity: 0;
transition: opacity var(--nav-trans-time) ease-in-out;
overflow: hidden;
&--nav-animate {
transition: opacity var(--nav-trans-time) ease-in-out;
}
&--nav-open {
opacity: 1;
@@ -18,7 +22,7 @@
&__header {
position: absolute;
top: 0;
width: 100%;
width: 100vw;
height: var(--app-header-height);
}
@@ -26,6 +30,7 @@
z-index: 1;
position: relative;
height: 100%;
width: 100%;
}
&__mobile-close {
@@ -33,7 +38,7 @@
background: none;
border: 0;
outline: 0;
padding: calc(var(--base) * 0.75) var(--gutter-h);
padding: base(0.8) 0;
}
&__scroll {
@@ -85,7 +90,9 @@
nav {
a {
position: relative;
padding: base(0.125) base(1.5) base(0.125) 0;
padding-block: base(0.125);
padding-inline-start: 0;
padding-inline-end: base(1.5);
display: flex;
text-decoration: none;
@@ -112,10 +119,16 @@
align-items: center;
}
&__link-icon {
margin-right: calc(var(--base) * 0.25);
top: -1px;
position: relative;
&__link-indicator {
position: absolute;
display: block;
// top: 0;
inset-inline-start: base(-1);
width: 2px;
height: 16px;
border-start-end-radius: 2px;
border-end-end-radius: 2px;
background: var(--theme-text);
}
@include mid-break {

View File

@@ -1,6 +1,7 @@
import type { ServerProps } from 'payload'
import { Logout } from '@payloadcms/ui'
import { RenderComponent, getCreateMappedComponent } from '@payloadcms/ui/shared'
import React from 'react'
import { NavHamburger } from './NavHamburger/index.js'
@@ -9,8 +10,6 @@ import './index.scss'
const baseClass = 'nav'
import { WithServerSideProps } from '@payloadcms/ui/shared'
import { DefaultNavClient } from './index.client.js'
export type NavProps = ServerProps
@@ -28,48 +27,38 @@ export const DefaultNav: React.FC<NavProps> = (props) => {
},
} = payload.config
const BeforeNavLinks = Array.isArray(beforeNavLinks)
? beforeNavLinks.map((Component, i) => (
<WithServerSideProps
Component={Component}
key={i}
serverOnlyProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
}}
/>
))
: null
const createMappedComponent = getCreateMappedComponent({
importMap: payload.importMap,
serverProps: {
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
},
})
const AfterNavLinks = Array.isArray(afterNavLinks)
? afterNavLinks.map((Component, i) => (
<WithServerSideProps
Component={Component}
key={i}
serverOnlyProps={{
i18n,
locale,
params,
payload,
permissions,
searchParams,
user,
}}
/>
))
: null
const mappedBeforeNavLinks = createMappedComponent(
beforeNavLinks,
undefined,
undefined,
'beforeNavLinks',
)
const mappedAfterNavLinks = createMappedComponent(
afterNavLinks,
undefined,
undefined,
'afterNavLinks',
)
return (
<NavWrapper baseClass={baseClass}>
<nav className={`${baseClass}__wrap`}>
{Array.isArray(BeforeNavLinks) && BeforeNavLinks.map((Component) => Component)}
<RenderComponent mappedComponent={mappedBeforeNavLinks} />
<DefaultNavClient />
{Array.isArray(AfterNavLinks) && AfterNavLinks.map((Component) => Component)}
<RenderComponent mappedComponent={mappedAfterNavLinks} />
<div className={`${baseClass}__controls`}>
<Logout />
</div>

View File

@@ -1,13 +1,12 @@
import type { AcceptedLanguages, I18nClient } from '@payloadcms/translations'
import type { PayloadRequest, SanitizedConfig } from 'payload'
import type { ImportMap, PayloadRequest, SanitizedConfig } from 'payload'
import { initI18n, rtlLanguages } from '@payloadcms/translations'
import { RootProvider } from '@payloadcms/ui'
import '@payloadcms/ui/scss/app.scss'
import { buildComponentMap } from '@payloadcms/ui/utilities/buildComponentMap'
import { createClientConfig } from '@payloadcms/ui/utilities/createClientConfig'
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
import { createClientConfig, createLocalReq, parseCookies } from 'payload'
import * as qs from 'qs-esm'
import { createLocalReq, parseCookies } from 'payload'
import React from 'react'
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
@@ -24,9 +23,11 @@ export const metadata = {
export const RootLayout = async ({
children,
config: configPromise,
importMap,
}: {
children: React.ReactNode
config: Promise<SanitizedConfig>
readonly children: React.ReactNode
readonly config: Promise<SanitizedConfig>
readonly importMap: ImportMap
}) => {
const config = await configPromise
@@ -67,7 +68,15 @@ export const RootLayout = async ({
)
const { permissions, user } = await payload.auth({ headers, req })
const clientConfig = await createClientConfig({ config, t: i18n.t })
const { clientConfig, render } = await createClientConfig({
DefaultEditView,
DefaultListView,
children,
config,
i18n,
importMap,
payload,
})
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
? 'RTL'
@@ -97,22 +106,47 @@ export const RootLayout = async ({
})
}
const { componentMap, wrappedChildren } = buildComponentMap({
DefaultEditView,
DefaultListView,
children,
i18n,
payload,
})
const navPreferences = user
? (
await payload.find({
collection: 'payload-preferences',
depth: 0,
limit: 1,
req,
user,
where: {
and: [
{
key: {
equals: 'nav',
},
},
{
'user.relationTo': {
equals: user.collection,
},
},
{
'user.value': {
equals: user.id,
},
},
],
},
})
)?.docs?.[0]
: null
const isNavOpen = (navPreferences?.value as any)?.open ?? true
return (
<html data-theme={theme} dir={dir} lang={languageCode}>
<body>
<RootProvider
componentMap={componentMap}
config={clientConfig}
dateFNSKey={i18n.dateFNSKey}
fallbackLang={clientConfig.i18n.fallbackLanguage}
isNavOpen={isNavOpen}
languageCode={languageCode}
languageOptions={languageOptions}
permissions={permissions}
@@ -121,7 +155,7 @@ export const RootLayout = async ({
translations={i18n.translations}
user={user}
>
{wrappedChildren}
{render}
</RootProvider>
<div id="portal" />
</body>

View File

@@ -20,6 +20,8 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
status: httpStatus.OK,
})
} catch (err) {
req.payload.logger.error({ err, msg: `There was an error building form state` })
if (err.message === 'Could not find field schema for given path') {
return Response.json(
{
@@ -39,8 +41,6 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
})
}
req.payload.logger.error({ err, msg: `There was an error building form state` })
return routeError({
config: req.payload.config,
err,

View File

@@ -1,7 +1,10 @@
import type { MappedComponent } from 'payload'
import { RenderComponent } from '@payloadcms/ui/shared'
import React from 'react'
export const OGImage: React.FC<{
Icon: React.ComponentType<any>
Icon: MappedComponent
description?: string
fontFamily?: string
leader?: string
@@ -82,7 +85,12 @@ export const OGImage: React.FC<{
width: '38px',
}}
>
<Icon fill="white" />
<RenderComponent
clientProps={{
fill: 'white',
}}
mappedComponent={Icon}
/>
</div>
</div>
)

View File

@@ -1,6 +1,6 @@
import type { PayloadRequest } from 'payload'
import { PayloadIcon } from '@payloadcms/ui/shared'
import { PayloadIcon, getCreateMappedComponent } from '@payloadcms/ui/shared'
import fs from 'fs/promises'
import { ImageResponse } from 'next/og.js'
import { NextResponse } from 'next/server.js'
@@ -32,7 +32,18 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => {
const hasLeader = searchParams.has('leader')
const leader = hasLeader ? searchParams.get('leader')?.slice(0, 100).replace('-', ' ') : ''
const description = searchParams.has('description') ? searchParams.get('description') : ''
const Icon = config.admin?.components?.graphics?.Icon || PayloadIcon
const createMappedComponent = getCreateMappedComponent({
importMap: req.payload.importMap,
serverProps: {},
})
const mappedIcon = createMappedComponent(
config.admin?.components?.graphics?.Icon,
undefined,
PayloadIcon,
'config.admin.components.graphics.Icon',
)
let fontData
@@ -50,7 +61,7 @@ export const generateOGImage = async ({ req }: { req: PayloadRequest }) => {
return new ImageResponse(
(
<OGImage
Icon={Icon}
Icon={mappedIcon}
description={description}
fontFamily={fontFamily}
leader={leader}

View File

@@ -38,19 +38,19 @@
--gutter-h: #{base(3)};
--spacing-view-bottom: var(--gutter-h);
--app-header-height: calc(var(--base) * 3);
--doc-controls-height: calc(var(--base) * 3);
--doc-controls-height: calc(var(--base) * 2.8);
--app-header-height: calc(var(--base) * 2.8);
--nav-width: 275px;
--nav-trans-time: 150ms;
@include mid-break {
--gutter-h: #{base(2)};
--app-header-height: calc(var(--base) * 2);
--doc-controls-height: calc(var(--base) * 2.5);
--app-header-height: calc(var(--base) * 2.4);
--doc-controls-height: calc(var(--base) * 2.4);
}
@include small-break {
--gutter-h: #{base(0.5)};
--gutter-h: #{base(0.8)};
--spacing-view-bottom: calc(var(--base) * 2);
--nav-width: 100vw;
}

View File

@@ -96,6 +96,7 @@
font-size: $baseline-body-size;
line-height: $baseline-px;
font-weight: normal;
font-family: var(--font-body);
}
%code {

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