Compare commits

..

38 Commits

Author SHA1 Message Date
Elliot DeNolf
c4ee623907 chore(release): v3.0.0-beta.75 [skip ci] 2024-08-07 14:01:09 -04:00
James Mikrut
1cb1e5e8b3 feat(db-*): allows for running migrations in production automatically (#7563)
## Description

Introduces a pattern for running migrations upon Payload init in
production.
2024-08-07 13:57:12 -04:00
Paul
e0699838e1 chore(ui): update useField jsdoc comment to point to the right internal hook (#7568) 2024-08-07 17:49:32 +00:00
Dan Ribbens
46f70d9df4 fix(db-postgres): #7492 migrate snapshots (#7540)
## Description

Fixes #7492 

In order to run createMigration, we need to read in the previous
snapshot file if one exists. When that snapshot was generated from an
older version of drizzle-kit, we have to first migrate it up match the
latest version for drizzle to generate the new migration. This change
adds in the call to check the version and migrate the snapshot if
needed.
2024-08-07 13:49:08 -04:00
Paul
b7e2c59622 fix(plugin-seo): issue with generating image from a function (#7566) 2024-08-07 17:35:43 +00:00
Jarrod Flesch
0cc7184023 fix: hydrate permissions on dashboard, fix active menu item logic 2024-08-07 12:14:58 -04:00
Jarrod Flesch
e905675a05 chore!: adjusts auth hydration from server (#7545)
Fixes https://github.com/payloadcms/payload/issues/6823

Allows the server to initialize the AuthProvider via props. Renames
`HydrateClientUser` to `HydrateAuthProvider`. It now only hydrates the
permissions as the user can be set from props. Permissions can be
initialized from props, but still need to be hydrated for some pages as
access control can be specific to docs/lists etc.

**BREAKING CHANGE**
- Renames exported `HydrateClientUser` to `HydrateAuthProvider`
2024-08-07 11:10:53 -04:00
Paul
4a20a63563 fix(ui): fixes issue when filtering by checkbox value in a different language (#7547)
Fixes https://github.com/payloadcms/payload/issues/7447
2024-08-07 00:48:50 +00:00
Paul
8d1fc6e8fb feat!: bump next canary to 104 and update withPayload for new config (#7541)
We are now bumping up the Next canary version to `15.0.0-canary.104` and
`react` and `react-dom` to `^19.0.0-rc-06d0b89e-20240801`.

Your new dependencies should look like this:
```
"next": "15.0.0-canary.104",
"react": "^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0-rc-06d0b89e-20240801",
```

---------

Co-authored-by: Alessio Gravili <alessio@gravili.de>
2024-08-06 23:54:34 +00:00
Patrik
62744e79ac fix(next, payload): enable relationship & upload version tracking when localization enabled (#7508) 2024-08-06 12:28:06 -04:00
Jarrod Flesch
e8bed7b315 chore: call refresh after the subscription is ready, fixes CI (#7542)
LivePreview data was stale if the user entered data while the socket
connection was being established. This change ensures fresh data is
fetched after the connection is established.

This is easy to see when turning on 4G connection and in CI, where it is
especially slow.
2024-08-06 12:17:50 -04:00
Alessio Gravili
f2b8ddb299 Merge PR: fix lexical upload html converter, export missing nodes #7539
Fixes https://github.com/payloadcms/payload/issues/7495

When the Upload HTML Converter was called from the local API, the upload
document did not populate properly due to overrideAccess not being
passed through to the dataloader. This PR also adds new properties to
the afterRead field hook, so that these can be used in the lexical html
field.

Reproduction here:
https://github.com/payloadcms/payload/tree/chore/reproduce-html-converter-issue

**BREAKING:** If you define your own, custom lexical HTML Converters
that have sub-nodes, or if you directly call the
`convertLexicalNodesToHTML` function anywhere, you now need to pass
through the `showHiddenFields`, draft and `overrideAccess` props to the
`convertLexicalNodesToHTML` function. These are available in the
arguments of your HTML Converter function
2024-08-06 12:04:56 -04:00
Alessio Gravili
ffd8ea516d feat(richtext-lexical): export serialized inline blocks and table node types 2024-08-06 11:42:09 -04:00
Paul
3bf09703e9 chore: turn off autocomplete for create first user form (#7538)
Turns off autocomplete on the first user form so it doesn't conflict
with wrong credentials being autofilled
2024-08-06 15:34:26 +00:00
Alessio Gravili
c15d679b65 fix(richtext-lexical)!: html converters not respecting overrideAccess property when populating values, in local API 2024-08-06 11:15:47 -04:00
Jarrod Flesch
a422a0d568 fix: scopes preferences queries and mutations by user (#7534)
Fixes https://github.com/payloadcms/payload/issues/7530

Properly scopes preferences queries/mutations by user.
2024-08-06 10:35:46 -04:00
Jarrod Flesch
edaeb1e29f fix: ensure pw confirmation when creating users in admin panel (#7535) 2024-08-06 10:31:08 -04:00
Jessica Chowdhury
6f35c356fe fix: custom meta icons getting overwritten by default icon (#7466)
## Description

Issue reported by Trading Point.

Payload favicon is still shown even when a custom icon is provided.

To replicate add to Payload config:
```ts
  admin: {
    meta: {
      icons: [
        {
          url: '/images/test.jpg',
          fetchPriority: 'high',
          sizes: '16x16',
        },
      ],
    },
  },
```

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

- [ ] 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-06 14:01:46 +00:00
Elliot DeNolf
0b9397399a chore(release): v3.0.0-beta.74 [skip ci] 2024-08-06 09:38:20 -04:00
Jarrod Flesch
cdcc35ccdb chore: fixes build error stemming from LoginField (#7532) 2024-08-06 09:15:21 -04:00
Jarrod Flesch
442189ec48 fix: email and username fields rendering in drawers (#7520)
Fixes https://github.com/payloadcms/payload/issues/7428

Now email and username fields are rendered with the RenderFields
component, making them behave similarly to other fields. They now appear
and can respect doc permissions, readOnly settings, etc.
2024-08-05 20:18:32 -04:00
Alessio Gravili
5d1cc760c9 fix(richtext-lexical): various table and icon style issues (#7522) 2024-08-05 22:10:18 +00:00
Alessio Gravili
2f90683c7d Merge PR: upgrade lexical, add table feature converter (#7521)
This PR
- upgrades lexical and ports all bug fixes from the playground over
- adds table action buttons. When hovering the edges of the table,
buttons pop up to easily add a new table column or row
- adds an html converter for the table feature
- makes the placeholder shown in the editor when no text is present
accessible

**BREAKING:** This upgrades lexical from 0.16.1 to 0.17.0. If you have
any lexical packages installed in your project, please update them
accordingly. Additionally, if you depend on the lexical APIs, please
consult their changelog, as lexical may introduce breaking changes:
https://github.com/facebook/lexical/releases/tag/v0.17.0
2024-08-05 17:18:57 -04:00
Patrik
3f5403a52a fix(ui): prevents hasMany text going outside of input boundaries (#7455)
## Description

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

`Before`:
![Screenshot 2024-07-31 at 12 40
50 PM](https://github.com/user-attachments/assets/ce61f4fc-e676-4273-aa4c-72610cb459b3)

`After`:
![Screenshot 2024-07-31 at 12 40
23 PM](https://github.com/user-attachments/assets/d92631eb-28fb-46ca-bc23-46c7916bba34)

- [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-05 17:10:35 -04:00
Alessio Gravili
9bccdfd60a feat(richtext-lexical): add HTML converter to table feature 2024-08-05 17:01:21 -04:00
Patrik
62666a9897 fix(ui): properly handles ID field component type based on payload.db.defaultIDType (#7416)
## Description

Fixes #7354 

Since the `defaultIDType` for IDs in `postgres` are of type `number` -
the `contains` operator should be available in the filter options.

This PR checks the `defaultIDType` of ID and properly outputs the
correct component type for IDs

I.e if ID is of type `number` - the filter operators for ID should
correspond to the the operators of type number as well

The `contains` operator only belongs on fields of type string, aka of
component type `text`

- [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-05 16:39:27 -04:00
Alessio Gravili
eb27b84854 chore(richtext-lexical): backport various minor bugfixes from lexical playground 2024-08-05 16:35:13 -04:00
Alessio Gravili
c3480811d3 feat(richtext-lexical): accessible editor placeholders 2024-08-05 16:19:02 -04:00
Alessio Gravili
12ba820de4 feat(richtext-lexical): add table hover actions to quickly add rows or columns 2024-08-05 16:08:31 -04:00
Elliot DeNolf
95fcd13929 fix(db-*): drizzle enums, bump drizzle-kit (#7514)
- bumps drizzle-kit
- Fixes https://github.com/payloadcms/payload/issues/7492 Enum issue.
2024-08-05 14:53:21 -04:00
Jarrod Flesch
6141c5950b chore: improves plugin creation docs (#7515) 2024-08-05 14:50:53 -04:00
Elliot DeNolf
0040e1756c fix(cpa): adjust template file location detection (#7507)
Adjust template file location detection. This was causing issues when
run with `pnpm create` because it is not run from a `dist` directory.

```
┌   create-payload-app
│
◇   ────────────────────────────────────────────╮
│                                               │
│  Welcome to Payload. Let's create a project!  │
│                                               │
├───────────────────────────────────────────────╯
│
▲  Payload installation detected in current project.
│
◇  Upgrade Payload in this project?
│  Yes
│
◇  Using pnpm.
│
│
◇  Updating 7 Payload packages to v3.0.0-beta.73...
│
│    - payload
│    - @payloadcms/db-mongodb
│    - @payloadcms/db-postgres
│    - @payloadcms/next
│    - @payloadcms/richtext-lexical
│    - @payloadcms/richtext-slate
│    - @payloadcms/ui
│
◇  Payload packages updated successfully.
│
◇  Updating Payload Next.js files...
│
■  ENOENT: no such file or directory, copyfile '/Users/elliot/Library/pnpm/store/v3/tmp/dlx-99797/node_modules/.pnpm/create-payload-app@3.0.0-beta.73/templates/blank-3.0/src/app/(payload)' -> '/Users/elliot/dev/payload-3.0-demo/src/app/(payload)'
```
2024-08-05 16:28:13 +00:00
Jarrod Flesch
1ebd54b315 feat: allows loginWithUsername to not require username (#7480)
Allows username to be optional when using the new loginWithUsername
feature. This can be done by the following:

```ts
auth: {
  loginWithUsername: {
    requireUsername: false, // <-- new property, default true
    requireEmail: false, // default: false
    allowEmailLogin: true, // default false
  },
},
```
2024-08-05 11:35:01 -04:00
Jessica Chowdhury
cdb2072a6d fix: error thrown in version view when localization is false (#7502)
## Description

`const localeValues = locales.map((locale) => locale.value)`

This line was previously throwing an error in the version view when
localization is false. Changed to ensure locales exist before mapping
over them.

- [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-05 15:10:02 +00:00
Tylan Davis
68553ff974 feat!: updated admin UI (#7424)
## Description

- Updates admin UI with more condensed spacing throughout.
- Improves hover states and read-only states for various components.
- Removes the `Merriweather` font from `next/font` and replaces with
stack of system serif fonts and fallbacks (Georgia, etc). Closes #7257

## BREAKING CHANGES
- Custom components and styling that don't utilize Payload's CSS/SCSS
variables may need adjustments to match the updated styling.
- If you are using the `Merriweather` font, you will need to manually
configure `next/font` in your own project.

---------

Co-authored-by: Paul Popus <paul@nouance.io>
2024-08-05 15:08:00 +00:00
Willy Brauner
9a3bce1118 feat: expose useTableColumns hook (#7448)
fix #4990 (v3)

## Description

Expose
[useTableColumns](b160686fff/packages/ui/src/elements/TableColumns/index.tsx (L25))
hook from client exported members of the ui packages.

The use of this hook, covered the case of custom ListView creation which
was not possible due to the lack of possibility to select a file if we
were in the "list-draw" view.

With `useTableColumns` we can execute the `onClick` defined in
`TableColumnsProvider` witch allows the selection on the clicked file.


b160686fff/packages/ui/src/elements/ListDrawer/DrawerContent.tsx (L290-L296)

## Use case

CustomListView.tsx:
```ts
const CustomListView = () => {
  // ...

  const tableColumns = useTableColumns()
  
  const handleItemClicked = (doc) => {
    const onClick = tableColumns.columns[0].cellProps?.onClick
    if (typeof onClick === 'function') {
      // we are in "list-drawer" view, execute the onClick function
      onClick({
        cellData: undefined,
        collectionSlug: doc,
        rowData: doc,
      })
    } else {
      // we are in "collection-admin" view, push the new route with next/navigation
      void router.push(`${collectionSlug}/${doc.id}`)
    }
  }
 
  return  <div className={"list"}>
            {data.docs?.length > 0 && (
              <RelationshipProvider>
                {docs.map((e, i) => (
                  <div className={"item"} key={i} onClick={() => handleItemClicked(e)}>
                     // ...
                  </div>
                ))}
              </RelationshipProvider>
            )}
          </div>
} 
```

This video shows the click of a file inside a CustomListView, in the
case of an "admin-collection" view then a "list-drawer" view.


https://github.com/user-attachments/assets/8aa17af5-a7aa-49de-b988-fc0db7ac8e47

- [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)
- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [x] Existing test suite passes locally with my changes
2024-08-05 14:52:47 +00:00
James Mikrut
005befcbe2 fix: #7488, cant deploy SQLite to Vercel (#7490)
## Description

Closes #7488 

Note - you'll also need to manually have `@libsql/client` installed in
your Next.js repository. This is not ideal, but it might be outside the
scope of what we can handle internally.

- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.
2024-08-05 10:41:12 -04:00
Alessio Gravili
e65b6478c9 feat(richtext-lexical)!: upgrade lexical from 0.16.1 to 0.17.0 2024-08-05 09:58:27 -04:00
356 changed files with 4499 additions and 2254 deletions

14
.vscode/launch.json vendored
View File

@@ -56,6 +56,20 @@
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js fields-relationship",
"cwd": "${workspaceFolder}",
"name": "Run Dev Fields-Relationship",
"request": "launch",
"type": "node-terminal"
},
{
"command": "node --no-deprecation test/dev.js login-with-username",
"cwd": "${workspaceFolder}",
"name": "Run Dev Login-With-Username",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev plugin-cloud-storage",
"cwd": "${workspaceFolder}",

View File

@@ -1,7 +1,7 @@
---
title: Building Your Own Plugin
label: Build Your Own
order: 50
order: 20
desc: Starting to build your own plugin? Find everything you need and learn best practices with the Payload plugin template.
keywords: plugins, template, config, configuration, extensions, custom, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
---
@@ -159,36 +159,60 @@ export const seed = async (payload: Payload): Promise<void> => {
```
## Overview of the src folder
## Building a Plugin
Now that we have our environment setup and dev project ready to go - it&apos;s time to build the plugin!
**index.ts**
First up, the `src/index.ts` file - this is where the plugin should be imported from. It is best practice not to build the plugin directly in this file, instead we use this to export the plugin and types from their respective files.
**Plugin.ts**
To reiterate, the essence of a [Payload Plugin](./overview) is simply to extend the [Payload Config](../configuration/overview) - and that is exactly what we are doing in this file.
```
import type { Config } from 'payload'
export const samplePlugin =
(pluginOptions: PluginTypes) =>
(incomingConfig: Config): Config => {
// create copy of incoming config
let config = { ...incomingConfig }
// do something cool with the config here
/**
* This is where you could modify the
* config based on the plugin options
*/
// If you wanted to add a new collection:
config.collections = [
...(config.collections || []),
newCollection,
]
// If you wanted to add a new global:
config.globals = [
...(config.globals || []),
newGlobal,
]
/**
* If you wanted to add a new field to a collection:
*
* 1. Loop over collections
* 2. Find the collection you want to add the field to
* 3. Add the field to the collection
*/
// If you wanted to add to the onInit:
config.onInit = async payload => {
if (incomingConfig.onInit) await incomingConfig.onInit(payload)
// Add additional onInit code here
}
// Finally, return the modified config
return config
}
```
1. First, you need to receive the existing Payload Config along with any plugin options.
2. Then set the variable `config` to be equal to a copy of the existing config.
3. From here, you can extend the config however you like!
4. Finally, return the config and you&apos;re all set.
To reiterate, the essence of a [Payload Plugin](./overview) is simply to extend the [Payload Config](../configuration/overview) - and that is exactly what we are doing in this file.
## Spread Syntax
### Spread syntax
[Spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) (or the spread operator) is a feature in JavaScript that uses the dot notation **(...)** to spread elements from arrays, strings, or objects into various contexts.
@@ -206,7 +230,7 @@ config.collections = [
First, you need to spread the `config.collections` to ensure that we don&apos;t lose the existing collections. Then you can add any additional collections, just as you would in a regular Payload Config.
This same logic is applied to other properties like admin, globals, hooks:
This same logic is applied to other array and object like properties such as admin, globals and hooks:
```
config.globals = [
@@ -220,7 +244,10 @@ config.hooks = {
}
```
Some properties will be slightly different to extend, for instance the `onInit` property:
### Extending functions
Function properties cannot use spread syntax. The way to extend them is to execute the existing function if it exists and then run your additional functionality.
Here is an example extending the `onInit` property:
```
config.onInit = async payload => {
@@ -231,10 +258,6 @@ config.onInit = async payload => {
}
```
If you wish to add to the `onInit`, you must include the async/await. We don&apos;t use spread syntax in this case, instead you must await the existing `onInit` before running additional functionality.
In the template, we have stubbed out a basic `onInitExtension` file that you can use, if not needed feel free to delete it.
## Types
If your plugin has options, you should define and provide types for these options in a separate file which gets exported from the main `index.ts`.

View File

@@ -1,7 +1,7 @@
---
title: Form Builder Plugin
label: Form Builder
order: 20
order: 40
desc: Easily build and manage forms from the Admin Panel. Send dynamic, personalized emails and even accept and process payments.
keywords: plugins, plugin, form, forms, form builder
---

View File

@@ -1,7 +1,7 @@
---
title: Nested Docs Plugin
label: Nested Docs
order: 20
order: 40
desc: Nested documents in a parent, child, and sibling relationship.
keywords: plugins, nested, documents, parent, child, sibling, relationship
---

View File

@@ -1,7 +1,7 @@
---
title: Redirects Plugin
label: Redirects
order: 20
order: 40
desc: Automatically create redirects for your Payload application
keywords: plugins, redirects, redirect, plugin, payload, cms, seo, indexing, search, search engine
---

View File

@@ -1,7 +1,7 @@
---
title: Search Plugin
label: Search
order: 20
order: 40
desc: Generates records of your documents that are extremely fast to search on.
keywords: plugins, search, search plugin, search engine, search index, search results, search bar, search box, search field, search form, search input
---

View File

@@ -1,7 +1,7 @@
---
title: Sentry Plugin
label: Sentry
order: 20
order: 40
desc: Integrate Sentry error tracking into your Payload application
keywords: plugins, sentry, error, tracking, monitoring, logging, bug, reporting, performance
---

View File

@@ -1,7 +1,7 @@
---
title: SEO Plugin
label: SEO
order: 20
order: 30
desc: Manage SEO metadata from your Payload admin
keywords: plugins, seo, meta, search, engine, ranking, google
---

View File

@@ -1,7 +1,7 @@
---
title: Stripe Plugin
label: Stripe
order: 20
order: 40
desc: Easily accept payments with Stripe
keywords: plugins, stripe, payments, ecommerce
---

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"private": true,
"type": "module",
"scripts": {
@@ -96,7 +96,7 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@libsql/client": "0.6.2",
"@next/bundle-analyzer": "15.0.0-canary.53",
"@next/bundle-analyzer": "15.0.0-canary.104",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/eslint-plugin": "workspace:*",
"@payloadcms/live-preview-react": "workspace:*",
@@ -131,15 +131,15 @@
"lint-staged": "15.2.7",
"minimist": "1.2.8",
"mongodb-memory-server": "^9.0",
"next": "15.0.0-canary.53",
"next": "15.0.0-canary.104",
"open": "^10.1.0",
"p-limit": "^5.0.0",
"playwright": "1.43.0",
"playwright-core": "1.43.0",
"prettier": "3.3.2",
"prompts": "2.4.2",
"react": "^19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0-rc-6230622a1a-20240610",
"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",
@@ -153,8 +153,8 @@
"typescript": "5.5.4"
},
"peerDependencies": {
"react": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610"
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801"
},
"engines": {
"node": "^18.20.2 || >=20.9.0",

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -74,9 +74,11 @@ export async function updatePayloadInProject(
info('Payload packages updated successfully.')
info(`Updating Payload Next.js files...`)
const templateFilesPath = dirname.endsWith('dist')
? path.resolve(dirname, '../..', 'dist/template')
: path.resolve(dirname, '../../../../templates/blank-3.0')
const templateFilesPath =
process.env.JEST_WORKER_ID !== undefined
? path.resolve(dirname, '../../../../templates/blank-3.0')
: path.resolve(dirname, '../..', 'dist/template')
const templateSrcDir = path.resolve(templateFilesPath, 'src/app/(payload)')

View File

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

View File

@@ -54,6 +54,10 @@ export const connect: Connect = async function connect(
this.payload.logger.info('---- DROPPED DATABASE ----')
}
}
if (process.env.NODE_ENV === 'production' && this.prodMigrations) {
await this.migrate({ migrations: this.prodMigrations })
}
} catch (err) {
console.log(err)
this.payload.logger.error(`Error: cannot connect to MongoDB. Details: ${err.message}`, err)

View File

@@ -8,7 +8,7 @@ import mongoose from 'mongoose'
import path from 'path'
import { createDatabaseAdapter } from 'payload'
import type { CollectionModel, GlobalModel } from './types.js'
import type { CollectionModel, GlobalModel, MigrateDownArgs, MigrateUpArgs } from './types.js'
import { connect } from './connect.js'
import { count } from './count.js'
@@ -78,6 +78,11 @@ export interface Args {
* typed as any to avoid dependency
*/
mongoMemoryServer?: MongoMemoryReplSet
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
transactionOptions?: TransactionOptions | false
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
url: false | string
@@ -90,6 +95,11 @@ export type MongooseAdapter = {
connection: Connection
globals: GlobalModel
mongoMemoryServer: MongoMemoryReplSet
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
sessions: Record<number | string, ClientSession>
versions: {
[slug: string]: CollectionModel
@@ -107,6 +117,11 @@ declare module 'payload' {
connection: Connection
globals: GlobalModel
mongoMemoryServer: MongoMemoryReplSet
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
sessions: Record<number | string, ClientSession>
transactionOptions: TransactionOptions
versions: {
@@ -121,6 +136,7 @@ export function mongooseAdapter({
disableIndexHints = false,
migrationDir: migrationDirArg,
mongoMemoryServer,
prodMigrations,
transactionOptions = {},
url,
}: Args): DatabaseAdapterObj {
@@ -167,6 +183,7 @@ export function mongooseAdapter({
migrateFresh,
migrationDir,
payload,
prodMigrations,
queryDrafts,
rollbackTransaction,
updateGlobal,

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -47,7 +47,7 @@
"dependencies": {
"@payloadcms/drizzle": "workspace:*",
"console-table-printer": "2.11.2",
"drizzle-kit": "0.23.1-7816536",
"drizzle-kit": "0.23.2-df9e596",
"drizzle-orm": "0.32.1",
"pg": "8.11.3",
"prompts": "2.4.2",

View File

@@ -91,4 +91,8 @@ export const connect: Connect = async function connect(
}
if (typeof this.resolveInitializing === 'function') this.resolveInitializing()
if (process.env.NODE_ENV === 'production' && this.prodMigrations) {
await this.migrate({ migrations: this.prodMigrations })
}
}

View File

@@ -1,10 +1,10 @@
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
import type { CreateMigration } from 'payload'
import fs from 'fs'
import { createRequire } from 'module'
import path from 'path'
import { getPredefinedMigration } from 'payload'
import { getPredefinedMigration, writeMigrationIndex } from 'payload'
import prompts from 'prompts'
import { fileURLToPath } from 'url'
@@ -25,7 +25,7 @@ export const createMigration: CreateMigration = async function createMigration(
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/payload')
const { generateDrizzleJson, generateMigration, upPgSnapshot } = require('drizzle-kit/api')
const drizzleJsonAfter = generateDrizzleJson(this.schema)
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
const formattedDate = yyymmdd.replace(/\D/g, '')
@@ -64,9 +64,11 @@ export const createMigration: CreateMigration = async function createMigration(
.reverse()?.[0]
if (latestSnapshot) {
drizzleJsonBefore = JSON.parse(
fs.readFileSync(`${dir}/${latestSnapshot}`, 'utf8'),
) as DrizzleSnapshotJSON
drizzleJsonBefore = JSON.parse(fs.readFileSync(`${dir}/${latestSnapshot}`, 'utf8'))
if (drizzleJsonBefore.version < drizzleJsonAfter.version) {
drizzleJsonBefore = upPgSnapshot(drizzleJsonBefore)
}
}
const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
@@ -113,5 +115,8 @@ export const createMigration: CreateMigration = async function createMigration(
upSQL: upSQL || ` // Migration code`,
}),
)
writeMigrationIndex({ migrationsDir: payload.db.migrationDir })
payload.logger.info({ msg: `Migration created at ${filePath}.ts` })
}

View File

@@ -1,4 +1,4 @@
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
export const defaultDrizzleSnapshot: DrizzleSnapshotJSON = {
id: '00000000-0000-0000-0000-000000000000',

View File

@@ -32,7 +32,7 @@ import {
updateOne,
updateVersion,
} from '@payloadcms/drizzle'
import { type PgSchema, pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { createDatabaseAdapter } from 'payload'
import type { Args, PostgresAdapter } from './types.js'
@@ -94,6 +94,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
pgSchema: adapterSchema,
pool: undefined,
poolOptions: args.pool,
prodMigrations: args.prodMigrations,
push: args.push,
relations: {},
relationshipsSuffix: args.relationshipsSuffix || '_rels',

View File

@@ -1,5 +1,5 @@
import type { TransactionPg } from '@payloadcms/drizzle/types'
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
import type { Payload, PayloadRequest } from 'payload'
import { sql } from 'drizzle-orm'
@@ -43,7 +43,7 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
const dir = payload.db.migrationDir
// get the drizzle migrateUpSQL from drizzle using the last schema
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/payload')
const { generateDrizzleJson, generateMigration } = require('drizzle-kit/api')
const drizzleJsonAfter = generateDrizzleJson(adapter.schema)
// Get the previous migration snapshot

View File

@@ -2,4 +2,4 @@ import type { RequireDrizzleKit } from '@payloadcms/drizzle/types'
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
export const requireDrizzleKit: RequireDrizzleKit = () => require('drizzle-kit/payload')
export const requireDrizzleKit: RequireDrizzleKit = () => require('drizzle-kit/api')

View File

@@ -4,7 +4,7 @@ import type {
DrizzleAdapter,
TransactionPg,
} from '@payloadcms/drizzle/types'
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
import type {
ColumnBaseConfig,
ColumnDataType,
@@ -33,6 +33,11 @@ export type Args = {
logger?: DrizzleConfig['logger']
migrationDir?: string
pool: PoolConfig
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
push?: boolean
relationshipsSuffix?: string
/**
@@ -136,6 +141,11 @@ export type PostgresAdapter = {
pgSchema?: Schema
pool: Pool
poolOptions: Args['pool']
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
push: boolean
rejectInitializing: () => void
relations: Record<string, GenericRelation>
@@ -178,6 +188,11 @@ declare module 'payload' {
pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool
poolOptions: Args['pool']
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
push: boolean
rejectInitializing: () => void
relationshipsSuffix?: string

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -46,7 +46,7 @@
"@libsql/client": "^0.6.2",
"@payloadcms/drizzle": "workspace:*",
"console-table-printer": "2.11.2",
"drizzle-kit": "0.23.1-7816536",
"drizzle-kit": "0.23.2-df9e596",
"drizzle-orm": "0.32.1",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",

View File

@@ -52,4 +52,8 @@ export const connect: Connect = async function connect(
}
if (typeof this.resolveInitializing === 'function') this.resolveInitializing()
if (process.env.NODE_ENV === 'production' && this.prodMigrations) {
await this.migrate({ migrations: this.prodMigrations })
}
}

View File

@@ -1,10 +1,10 @@
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
import type { CreateMigration } from 'payload'
import fs from 'fs'
import { createRequire } from 'module'
import path from 'path'
import { getPredefinedMigration } from 'payload'
import { getPredefinedMigration, writeMigrationIndex } from 'payload'
import prompts from 'prompts'
import { fileURLToPath } from 'url'
@@ -25,7 +25,7 @@ export const createMigration: CreateMigration = async function createMigration(
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
const { generateSQLiteDrizzleJson, generateSQLiteMigration } = require('drizzle-kit/payload')
const { generateSQLiteDrizzleJson, generateSQLiteMigration } = require('drizzle-kit/api')
const drizzleJsonAfter = await generateSQLiteDrizzleJson(this.schema)
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
const formattedDate = yyymmdd.replace(/\D/g, '')
@@ -112,5 +112,8 @@ export const createMigration: CreateMigration = async function createMigration(
upSQL: upSQL || ` // Migration code`,
}),
)
writeMigrationIndex({ migrationsDir: payload.db.migrationDir })
payload.logger.info({ msg: `Migration created at ${filePath}.ts` })
}

View File

@@ -1,4 +1,4 @@
import type { DrizzleSQLiteSnapshotJSON } from 'drizzle-kit/payload'
import type { DrizzleSQLiteSnapshotJSON } from 'drizzle-kit/api'
export const defaultDrizzleSnapshot: DrizzleSQLiteSnapshotJSON = {
id: '00000000-0000-0000-0000-000000000000',

View File

@@ -93,6 +93,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger,
operators,
prodMigrations: args.prodMigrations,
push: args.push,
relations: {},
relationshipsSuffix: args.relationshipsSuffix || '_rels',

View File

@@ -10,6 +10,6 @@ export const requireDrizzleKit: RequireDrizzleKit = () => {
const {
generateSQLiteDrizzleJson: generateDrizzleJson,
pushSQLiteSchema: pushSchema,
} = require('drizzle-kit/payload')
} = require('drizzle-kit/api')
return { generateDrizzleJson, pushSchema }
}

View File

@@ -1,4 +1,4 @@
import type { AnySQLiteColumn} from 'drizzle-orm/sqlite-core';
import type { AnySQLiteColumn } from 'drizzle-orm/sqlite-core'
import { index, uniqueIndex } from 'drizzle-orm/sqlite-core'

View File

@@ -18,6 +18,11 @@ export type Args = {
localesSuffix?: string
logger?: DrizzleConfig['logger']
migrationDir?: string
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
push?: boolean
relationshipsSuffix?: string
schemaName?: string
@@ -100,6 +105,11 @@ export type SQLiteAdapter = {
localesSuffix?: string
logger: DrizzleConfig['logger']
operators: Operators
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
push: boolean
rejectInitializing: () => void
relations: Record<string, GenericRelation>
@@ -139,6 +149,11 @@ declare module 'payload' {
initializing: Promise<void>
localesSuffix?: string
logger: DrizzleConfig['logger']
prodMigrations?: {
down: (args: MigrateDownArgs) => Promise<void>
name: string
up: (args: MigrateUpArgs) => Promise<void>
}[]
push: boolean
rejectInitializing: () => void
relationshipsSuffix?: string

View File

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

View File

@@ -1,4 +1,3 @@
import type { Payload, PayloadRequest } from 'payload'
import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload'
@@ -9,9 +8,12 @@ import type { DrizzleAdapter, Migration } from './types.js'
import { migrationTableExists } from './utilities/migrationTableExists.js'
import { parseError } from './utilities/parseError.js'
export async function migrate(this: DrizzleAdapter): Promise<void> {
export const migrate: DrizzleAdapter['migrate'] = async function migrate(
this: DrizzleAdapter,
args,
): Promise<void> {
const { payload } = this
const migrationFiles = await readMigrationFiles({ payload })
const migrationFiles = args?.migrations || (await readMigrationFiles({ payload }))
if (!migrationFiles.length) {
payload.logger.info({ msg: 'No migrations to run.' })
@@ -64,7 +66,7 @@ export async function migrate(this: DrizzleAdapter): Promise<void> {
// If already ran, skip
if (alreadyRan) {
continue
continue
}
await runMigrationFile(payload, migration, newBatch)

View File

@@ -42,7 +42,7 @@ export async function migrateFresh(
await this.dropDatabase({ adapter: this })
const migrationFiles = (await readMigrationFiles({ payload })) as Migration[]
const migrationFiles = await readMigrationFiles({ payload })
payload.logger.debug({
msg: `Found ${migrationFiles.length} migration files.`,
})

View File

@@ -133,7 +133,7 @@ export type Migration = {
db?: DrizzleTransaction | LibSQLDatabase<Record<string, never>> | PostgresDB
payload: Payload
req: PayloadRequest
}) => Promise<boolean>
}) => Promise<void>
up: ({
db,
payload,
@@ -142,7 +142,7 @@ export type Migration = {
db?: DrizzleTransaction | LibSQLDatabase | PostgresDB
payload: Payload
req: PayloadRequest
}) => Promise<boolean>
}) => Promise<void>
} & MigrationData
export type CreateJSONQueryArgs = {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"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.73",
"version": "3.0.0-beta.75",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {
@@ -41,8 +41,8 @@
"payload": "workspace:*"
},
"peerDependencies": {
"react": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610",
"react-dom": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610"
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801"
},
"publishConfig": {
"exports": {

View File

@@ -39,6 +39,9 @@ export const RefreshRouteOnSave: React.FC<{
ready({
serverURL,
})
// refresh after the ready message is sent to get the latest data
refresh()
}
return () => {
@@ -46,7 +49,7 @@ export const RefreshRouteOnSave: React.FC<{
window.removeEventListener('message', onMessage)
}
}
}, [serverURL, onMessage, depth, apiRoute])
}, [serverURL, onMessage, depth, apiRoute, refresh])
return null
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"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.73",
"version": "3.0.0-beta.75",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -94,7 +94,7 @@
},
"peerDependencies": {
"graphql": "^16.8.1",
"next": "^15.0.0-canary.53",
"next": "^15.0.0-canary.104",
"payload": "workspace:*"
},
"engines": {

View File

@@ -35,7 +35,7 @@
position: absolute;
width: 100%;
height: 100%;
border-radius: 2px;
border-radius: var(--style-radius-s);
background-color: var(--theme-elevation-50);
opacity: 0;
}
@@ -51,6 +51,7 @@
}
&--active {
font-weight: 600;
&::before {
opacity: 1;
background-color: var(--theme-elevation-100);
@@ -78,14 +79,15 @@
gap: 4px;
width: 100%;
height: 100%;
padding: calc(var(--base) / 2) calc(var(--base));
line-height: base(1.2);
padding: base(0.2) base(0.6);
}
&__count {
min-width: 22px;
line-height: base(0.8);
min-width: base(0.8);
text-align: center;
padding: 2px 7px;
background-color: var(--theme-elevation-100);
border-radius: 1px;
border-radius: var(--style-radius-s);
}
}

View File

@@ -0,0 +1,103 @@
'use client'
import type { FieldPermissions, LoginWithUsernameOptions } from 'payload'
import { EmailField, RenderFields, TextField, useTranslation } from '@payloadcms/ui'
import { email, username } from 'payload/shared'
import React from 'react'
type Props = {
loginWithUsername?: LoginWithUsernameOptions | false
}
function EmailFieldComponent(props: Props) {
const { loginWithUsername } = props
const { t } = useTranslation()
const requireEmail = !loginWithUsername || (loginWithUsername && loginWithUsername.requireEmail)
const showEmailField =
!loginWithUsername || loginWithUsername?.requireEmail || loginWithUsername?.allowEmailLogin
if (showEmailField) {
return (
<EmailField
autoComplete="off"
label={t('general:email')}
name="email"
path="email"
required={requireEmail}
validate={email}
/>
)
}
return null
}
function UsernameFieldComponent(props: Props) {
const { loginWithUsername } = props
const { t } = useTranslation()
const requireUsername = loginWithUsername && loginWithUsername.requireUsername
const showUsernameField = Boolean(loginWithUsername)
if (showUsernameField) {
return (
<TextField
label={t('authentication:username')}
name="username"
path="username"
required={requireUsername}
validate={username}
/>
)
}
return null
}
type RenderEmailAndUsernameFieldsProps = {
className?: string
loginWithUsername?: LoginWithUsernameOptions | false
operation?: 'create' | 'update'
permissions?: {
[fieldName: string]: FieldPermissions
}
readOnly: boolean
}
export function RenderEmailAndUsernameFields(props: RenderEmailAndUsernameFieldsProps) {
const { className, loginWithUsername, operation, permissions, readOnly } = props
return (
<RenderFields
className={className}
fieldMap={[
{
name: 'email',
type: 'text',
CustomField: <EmailFieldComponent loginWithUsername={loginWithUsername} />,
cellComponentProps: null,
fieldComponentProps: { type: 'email', autoComplete: 'off', readOnly },
fieldIsPresentational: false,
isFieldAffectingData: true,
localized: false,
},
{
name: 'username',
type: 'text',
CustomField: <UsernameFieldComponent loginWithUsername={loginWithUsername} />,
cellComponentProps: null,
fieldComponentProps: { type: 'text', readOnly },
fieldIsPresentational: false,
isFieldAffectingData: true,
localized: false,
},
]}
forceRender
operation={operation}
path=""
permissions={permissions}
readOnly={readOnly}
schemaPath=""
/>
)
}

View File

@@ -37,10 +37,12 @@ const Component: React.FC<{
<p>{t('general:changesNotSaved')}</p>
</div>
<div className={`${baseClass}__controls`}>
<Button buttonStyle="secondary" onClick={onCancel}>
<Button buttonStyle="secondary" onClick={onCancel} size="large">
{t('general:stayOnThisPage')}
</Button>
<Button onClick={onConfirm}>{t('general:leaveAnyway')}</Button>
<Button onClick={onConfirm} size="large">
{t('general:leaveAnyway')}
</Button>
</div>
</div>
</Modal>

View File

@@ -14,6 +14,7 @@ import {
} from '@payloadcms/ui'
import { EntityType, formatAdminURL, groupNavItems } from '@payloadcms/ui/shared'
import LinkWithDefault from 'next/link.js'
import { usePathname } from 'next/navigation.js'
import React, { Fragment } from 'react'
const baseClass = 'nav'
@@ -21,6 +22,7 @@ const baseClass = 'nav'
export const DefaultNavClient: React.FC = () => {
const { permissions } = useAuth()
const { isEntityVisible } = useEntityVisibility()
const pathname = usePathname()
const {
collections,
@@ -84,17 +86,11 @@ export const DefaultNavClient: React.FC = () => {
LinkWithDefault) as typeof LinkWithDefault.default
const LinkElement = Link || 'a'
const activeCollection = window?.location?.pathname
?.split('/')
.find(
(_, index, arr) =>
arr[index - 1] === 'collections' || arr[index - 1] === 'globals',
)
const activeCollection = pathname.endsWith(href)
return (
<LinkElement
className={[`${baseClass}__link`, activeCollection === entity?.slug && `active`]
className={[`${baseClass}__link`, activeCollection && `active`]
.filter(Boolean)
.join(' ')}
href={href}
@@ -102,9 +98,11 @@ export const DefaultNavClient: React.FC = () => {
key={i}
tabIndex={!navOpen ? -1 : undefined}
>
<span className={`${baseClass}__link-icon`}>
<ChevronIcon direction="right" />
</span>
{activeCollection && (
<span className={`${baseClass}__link-icon`}>
<ChevronIcon direction="right" />
</span>
)}
<span className={`${baseClass}__link-label`}>{entityLabel}</span>
</LinkElement>
)

View File

@@ -110,16 +110,9 @@
&__link {
display: flex;
align-items: center;
&.active {
.nav__link-icon {
display: block;
}
}
}
&__link-icon {
display: none;
margin-right: calc(var(--base) * 0.25);
top: -1px;
position: relative;

View File

@@ -1,13 +1,13 @@
import type { AcceptedLanguages, I18nClient } from '@payloadcms/translations'
import type { SanitizedConfig } from 'payload'
import type { 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 { Merriweather } from 'next/font/google'
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
import { createClientConfig, parseCookies } from 'payload'
import { createClientConfig, createLocalReq, parseCookies } from 'payload'
import * as qs from 'qs-esm'
import React from 'react'
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
@@ -16,14 +16,6 @@ import { getRequestTheme } from '../../utilities/getRequestTheme.js'
import { DefaultEditView } from '../../views/Edit/Default/index.js'
import { DefaultListView } from '../../views/List/Default/index.js'
const merriweather = Merriweather({
display: 'swap',
style: ['normal', 'italic'],
subsets: ['latin'],
variable: '--font-serif',
weight: ['400', '900'],
})
export const metadata = {
description: 'Generated by Next.js',
title: 'Next.js',
@@ -61,6 +53,20 @@ export const RootLayout = async ({
language: languageCode,
})
const req = await createLocalReq(
{
fallbackLocale: null,
req: {
headers,
host: headers.get('host'),
i18n,
url: `${payload.config.serverURL}`,
} as PayloadRequest,
},
payload,
)
const { permissions, user } = await payload.auth({ headers, req })
const clientConfig = await createClientConfig({ config, t: i18n.t })
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
@@ -100,7 +106,7 @@ export const RootLayout = async ({
})
return (
<html className={merriweather.variable} data-theme={theme} dir={dir} lang={languageCode}>
<html data-theme={theme} dir={dir} lang={languageCode}>
<body>
<RootProvider
componentMap={componentMap}
@@ -109,9 +115,11 @@ export const RootLayout = async ({
fallbackLang={clientConfig.i18n.fallbackLanguage}
languageCode={languageCode}
languageOptions={languageOptions}
permissions={permissions}
switchLanguageServerAction={switchLanguageServerAction}
theme={theme}
translations={i18n.translations}
user={user}
>
{wrappedChildren}
</RootProvider>

View File

@@ -1,9 +1,9 @@
@import './styles.scss';
@import 'styles';
@import './toasts.scss';
@import './colors.scss';
:root {
--base-px: 25;
--base-px: 20;
--base-body-size: 13;
--base: calc((var(--base-px) / var(--base-body-size)) * 1rem);
@@ -21,6 +21,7 @@
--theme-baseline-body-size: #{$baseline-body-size};
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif;
--font-serif: Georgia, 'Bitstream Charter', 'Charis SIL', Utopia, 'URW Bookman L', serif;
--font-mono: monospace;
--style-radius-s: #{$style-radius-s};
@@ -67,12 +68,6 @@ html {
@extend %body;
background: var(--theme-bg);
-webkit-font-smoothing: antialiased;
opacity: 0;
&[data-theme='dark'],
&[data-theme='light'] {
opacity: initial;
}
&[data-theme='dark'] {
--theme-bg: var(--theme-elevation-0);
@@ -111,12 +106,12 @@ body {
}
::selection {
background: var(--theme-success-500);
background: var(--color-success-250);
color: var(--theme-base-800);
}
::-moz-selection {
background: var(--theme-success-500);
background: var(--color-success-250);
color: var(--theme-base-800);
}

View File

@@ -0,0 +1,59 @@
@import 'vars';
@import 'queries';
.Toastify {
.Toastify__toast-container {
left: base(5);
transform: none;
right: base(5);
width: auto;
}
.Toastify__toast {
padding: base(0.5);
border-radius: $style-radius-m;
font-weight: 600;
}
.Toastify__close-button {
align-self: center;
opacity: 0.7;
&:hover {
opacity: 1;
}
}
.Toastify__toast--success {
color: var(--color-success-900);
background: var(--color-success-500);
.Toastify__progress-bar {
background-color: var(--color-success-900);
}
}
.Toastify__close-button--success {
color: var(--color-success-900);
}
.Toastify__toast--error {
background: var(--theme-error-500);
color: #fff;
.Toastify__progress-bar {
background-color: #fff;
}
}
.Toastify__close-button--light {
color: inherit;
}
@include mid-break {
.Toastify__toast-container {
left: $baseline;
right: $baseline;
}
}
}

View File

@@ -1,24 +1,28 @@
@import './styles.scss';
.payload-toast-container {
padding: 0;
margin: 0;
.payload-toast-close-button {
position: absolute;
order: 3;
left: unset;
right: 0.5rem;
top: 1.55rem;
color: var(--theme-elevation-400);
inset-inline-end: base(0.5);
top: 50%;
transform: translateY(-50%);
color: var(--theme-elevation-600);
background: unset;
border: none;
display: flex;
width: 1.25rem;
height: 1.25rem;
justify-content: center;
align-items: center;
&:hover {
background: none;
}
svg {
width: 2rem;
height: 2rem;
width: base(0.75);
height: base(0.75);
}
&:hover {
color: var(--theme-elevation-250);
background: none;
}
[dir='RTL'] & {
@@ -27,16 +31,20 @@
}
}
.toast-title {
line-height: base(1);
}
.payload-toast-item {
padding: 1rem 2.5rem 1rem 1rem;
color: var(--theme-text);
padding: base(0.5);
color: var(--theme-elevation-800);
font-style: normal;
font-weight: 600;
display: flex;
gap: 1rem;
align-items: center;
width: 100%;
border-radius: 0.15rem;
border-radius: 4px;
border: 1px solid var(--theme-border-color);
background: var(--theme-input-bg);
box-shadow:
@@ -45,6 +53,7 @@
.toast-content {
transition: opacity 100ms cubic-bezier(0.55, 0.055, 0.675, 0.19);
width: 100%;
}
&[data-front='false'] {
@@ -60,51 +69,72 @@
}
.toast-icon {
svg {
width: 2.4rem;
height: 2.4rem;
width: base(1);
height: base(1);
margin: 0;
display: flex;
align-items: center;
justify-content: center;
& > * {
width: base(1.2);
height: base(1.2);
}
}
&.toast-warning {
border-color: var(--theme-warning-200);
background-color: var(--theme-warning-100);
color: var(--theme-warning-800);
border-color: var(--theme-warning-150);
background-color: var(--theme-warning-50);
.payload-toast-close-button {
color: var(--theme-warning-600);
&:hover {
color: var(--theme-warning-250);
}
}
}
&.toast-error {
border-color: var(--theme-error-300);
background-color: var(--theme-error-150);
color: var(--theme-error-800);
border-color: var(--theme-error-150);
background-color: var(--theme-error-50);
.payload-toast-close-button {
color: var(--theme-error-600);
&:hover {
color: var(--theme-error-250);
}
}
}
&.toast-success {
border-color: var(--theme-success-200);
background-color: var(--theme-success-100);
color: var(--theme-success-800);
border-color: var(--theme-success-150);
background-color: var(--theme-success-50);
.payload-toast-close-button {
color: var(--theme-success-600);
&:hover {
color: var(--theme-success-250);
}
}
}
&.toast-info {
border-color: var(--theme-elevation-250);
background-color: var(--theme-elevation-100);
}
color: var(--theme-elevation-800);
border-color: var(--theme-elevation-150);
background-color: var(--theme-elevation-50);
[data-theme='light'] & {
&.toast-warning {
border-color: var(--theme-warning-550);
background-color: var(--theme-warning-100);
}
.payload-toast-close-button {
color: var(--theme-elevation-600);
&.toast-error {
border-color: var(--theme-error-200);
background-color: var(--theme-error-50);
}
&.toast-success {
border-color: var(--theme-success-550);
background-color: var(--theme-success-50);
}
&.toast-info {
border-color: var(--theme-border-color);
background-color: var(--theme-elevation-50);
&:hover {
color: var(--theme-elevation-250);
}
}
}
}

View File

@@ -15,17 +15,10 @@
font-weight: 500;
}
%jumbo {
font-size: base(2.5);
line-height: 1;
margin: 0 0 base(2);
}
%h1 {
margin: 0 0 base(1);
font-size: base(2);
line-height: 1.15;
letter-spacing: -1px;
font-size: base(1.6);
line-height: base(1.8);
@include small-break {
letter-spacing: -0.5px;
@@ -35,9 +28,8 @@
%h2 {
margin: 0 0 base(1);
font-size: base(1.25);
line-height: 1.15;
letter-spacing: -0.5px;
font-size: base(1.3);
line-height: base(1.6);
@include small-break {
font-size: base(0.85);
@@ -46,9 +38,8 @@
%h3 {
margin: 0 0 base(1);
font-size: base(0.925);
line-height: 1.25;
letter-spacing: -0.5px;
font-size: base(1);
line-height: base(1.2);
@include small-break {
font-size: base(0.65);
@@ -58,27 +49,27 @@
%h4 {
margin: 0 0 $baseline;
font-size: base(0.75);
line-height: 1.5;
font-size: base(0.8);
line-height: base(1);
letter-spacing: -0.375px;
}
%h5 {
margin: 0;
font-size: base(0.5625);
line-height: 1.5;
font-size: base(0.65);
line-height: base(0.8);
}
%h6 {
margin: 0;
font-size: base(0.5);
line-height: 1.5;
font-size: base(0.6);
line-height: base(0.8);
}
%small {
margin: 0;
font-size: 11px;
line-height: 1.5;
font-size: 12px;
line-height: 20px;
}
/////////////////////////////

View File

@@ -13,7 +13,7 @@ $breakpoint-l-width: 1440px !default;
// BASELINE GRID
//////////////////////////////
$baseline-px: 25px !default;
$baseline-px: 20px !default;
$baseline-body-size: 13px !default;
$baseline: math.div($baseline-px, $baseline-body-size) + rem;
@@ -40,7 +40,7 @@ $color-purple: #f3ddf3 !default;
$style-radius-s: 3px !default;
$style-radius-m: 4px !default;
$style-radius-l: 9px !default;
$style-radius-l: 8px !default;
$style-stroke-width: 1px !default;
$style-stroke-width-s: 1px !default;
@@ -50,8 +50,8 @@ $style-stroke-width-m: 2px !default;
// MISC
//////////////////////////////
$top-header-offset: calc(var(--base) - 1px);
$top-header-offset-m: calc(var(--base) * 3);
$top-header-offset: calc(base(1) - 1px);
$top-header-offset-m: base(3);
$focus-box-shadow: 0 0 0 $style-stroke-width-m var(--theme-success-500);
//////////////////////////////
@@ -59,41 +59,19 @@ $focus-box-shadow: 0 0 0 $style-stroke-width-m var(--theme-success-500);
//////////////////////////////
@mixin shadow-sm {
box-shadow:
0 2px 3px 0 rgba(0, 2, 4, 0.05),
0 10px 4px -8px rgba(0, 2, 4, 0.02);
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1);
}
@mixin shadow-m {
box-shadow:
0 0 30px 0 rgb(0 2 4 / 12%),
0 30px 25px -8px rgb(0 2 4 / 10%);
box-shadow: 0 4px 8px -3px rgba(0, 0, 0, 0.1);
}
@mixin shadow-lg {
box-shadow:
0 20px 35px -10px rgba(0, 2, 4, 0.2),
0 6px 4px -4px rgba(0, 2, 4, 0.02);
box-shadow: 0 -2px 16px -2px rgba(0, 0, 0, 0.2);
}
@mixin shadow-lg-top {
box-shadow:
0 -20px 35px -10px rgba(0, 2, 4, 0.2),
0 -6px 4px -4px rgba(0, 2, 4, 0.02);
}
@mixin shadow {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.07);
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
@mixin inputShadowActive {
box-shadow:
0 2px 3px 0 rgba(0, 2, 4, 0.16),
0 6px 4px -4px rgba(0, 2, 4, 0.13);
box-shadow: 0 2px 16px -2px rgba(0, 0, 0, 0.2);
}
@mixin inputShadow {
@@ -101,15 +79,7 @@ $focus-box-shadow: 0 0 0 $style-stroke-width-m var(--theme-success-500);
&:not(:disabled) {
&:hover {
box-shadow:
0 2px 3px 0 rgba(0, 2, 4, 0.13),
0 6px 4px -4px rgba(0, 2, 4, 0.1);
}
&:active,
&:focus-within,
&:focus {
@include inputShadowActive;
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.2);
}
}
}
@@ -147,19 +117,33 @@ $focus-box-shadow: 0 0 0 $style-stroke-width-m var(--theme-success-500);
@include blur-bg(var(--theme-bg), 0.3);
}
@mixin readOnly {
background: var(--theme-elevation-100);
color: var(--theme-elevation-400);
box-shadow: none;
&:hover {
border-color: var(--theme-elevation-150);
box-shadow: none;
}
}
@mixin formInput() {
@include inputShadow;
font-family: var(--font-body);
width: 100%;
border: 1px solid var(--theme-elevation-150);
border-radius: var(--style-radius-s);
background: var(--theme-input-bg);
color: var(--theme-elevation-800);
border-radius: 0;
font-size: 1rem;
height: base(2);
line-height: base(1);
padding: base(0.5) base(0.75);
padding: base(0.4) base(0.75);
-webkit-appearance: none;
transition-property: border, box-shadow;
transition-duration: 100ms;
transition-timing-function: cubic-bezier(0, 0.2, 0.2, 1);
&[data-rtl='true'] {
direction: rtl;
@@ -189,12 +173,7 @@ $focus-box-shadow: 0 0 0 $style-stroke-width-m var(--theme-success-500);
}
&:disabled {
background: var(--theme-elevation-200);
color: var(--theme-elevation-450);
&:hover {
border-color: var(--theme-elevation-150);
}
@include readOnly;
}
}

View File

@@ -1,6 +1,5 @@
import type { Metadata } from 'next'
import type { Icon } from 'next/dist/lib/metadata/types/metadata-types.js'
import type { MetaConfig } from 'payload'
import type { IconConfig, MetaConfig } from 'payload'
import { payloadFaviconDark, payloadFaviconLight, staticOGImage } from '@payloadcms/ui/assets'
import * as qs from 'qs-esm'
@@ -24,7 +23,7 @@ export const meta = async (args: { serverURL: string } & MetaConfig): Promise<an
titleSuffix,
} = args
const payloadIcons: Icon[] = [
const payloadIcons: IconConfig[] = [
{
type: 'image/png',
rel: 'icon',
@@ -40,10 +39,10 @@ export const meta = async (args: { serverURL: string } & MetaConfig): Promise<an
},
]
let icons = customIcons ?? payloadIcons // TODO: fix this type assertion
let icons = payloadIcons
if (customIcons && typeof customIcons === 'object' && Array.isArray(customIcons)) {
icons = payloadIcons.concat(customIcons) // TODO: fix this type assertion
icons = customIcons
}
const metaTitle = `${title} ${titleSuffix}`

View File

@@ -1,6 +1,6 @@
import type { AdminViewProps, ServerSideEditViewProps } from 'payload'
import { DocumentInfoProvider, HydrateClientUser } from '@payloadcms/ui'
import { DocumentInfoProvider, HydrateAuthProvider } from '@payloadcms/ui'
import { RenderCustomComponent } from '@payloadcms/ui/shared'
import { notFound } from 'next/navigation.js'
import React from 'react'
@@ -82,7 +82,7 @@ export const Account: React.FC<AdminViewProps> = async ({
i18n={i18n}
permissions={permissions}
/>
<HydrateClientUser permissions={permissions} user={user} />
<HydrateAuthProvider permissions={permissions} />
<RenderCustomComponent
CustomComponent={
typeof CustomAccountComponent === 'function' ? CustomAccountComponent : undefined

View File

@@ -1,5 +1,5 @@
'use client'
import type { FormState } from 'payload'
import type { FormState, LoginWithUsernameOptions } from 'payload'
import {
ConfirmPasswordField,
@@ -15,14 +15,13 @@ import {
import { getFormState } from '@payloadcms/ui/shared'
import React from 'react'
import { LoginField } from '../Login/LoginField/index.js'
import { RenderEmailAndUsernameFields } from '../../elements/EmailAndUsername/index.js'
export const CreateFirstUserClient: React.FC<{
initialState: FormState
loginType: 'email' | 'emailOrUsername' | 'username'
requireEmail?: boolean
loginWithUsername?: LoginWithUsernameOptions | false
userSlug: string
}> = ({ initialState, loginType, requireEmail = true, userSlug }) => {
}> = ({ initialState, loginWithUsername, userSlug }) => {
const { getFieldMap } = useComponentMap()
const {
@@ -58,11 +57,14 @@ export const CreateFirstUserClient: React.FC<{
redirect={admin}
validationOperation="create"
>
{['emailOrUsername', 'username'].includes(loginType) && <LoginField type="username" />}
{['email', 'emailOrUsername'].includes(loginType) && (
<LoginField required={requireEmail} type="email" />
)}
<RenderEmailAndUsernameFields
className="emailAndUsername"
loginWithUsername={loginWithUsername}
operation="create"
readOnly={false}
/>
<PasswordField
autoComplete="off"
label={t('authentication:newPassword')}
name="password"
path="password"
@@ -77,7 +79,7 @@ export const CreateFirstUserClient: React.FC<{
readOnly={false}
schemaPath={userSlug}
/>
<FormSubmit>{t('general:create')}</FormSubmit>
<FormSubmit size="large">{t('general:create')}</FormSubmit>
</Form>
)
}

View File

@@ -3,3 +3,7 @@
margin-bottom: var(--base);
}
}
.emailAndUsername {
margin-bottom: var(--base);
}

View File

@@ -27,12 +27,6 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
const collectionConfig = config.collections?.find((collection) => collection?.slug === userSlug)
const { auth: authOptions } = collectionConfig
const loginWithUsername = authOptions.loginWithUsername
const emailRequired = loginWithUsername && loginWithUsername.requireEmail
let loginType: LoginFieldProps['type'] = loginWithUsername ? 'username' : 'email'
if (loginWithUsername && (loginWithUsername.allowEmailLogin || loginWithUsername.requireEmail)) {
loginType = 'emailOrUsername'
}
const { formState } = await getDocumentData({
collectionConfig,
@@ -47,8 +41,7 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
<p>{req.t('authentication:beginCreateFirstUser')}</p>
<CreateFirstUserClient
initialState={formState}
loginType={loginType}
requireEmail={emailRequired}
loginWithUsername={loginWithUsername}
userSlug={userSlug}
/>
</div>

View File

@@ -26,13 +26,9 @@
padding: 0;
margin: 0;
list-style: none;
display: flex;
gap: var(--gap);
flex-wrap: wrap;
li {
width: calc(100% / var(--cols) - var(--gap) / var(--cols) * (var(--cols) - 1));
}
display: grid;
grid-template-columns: repeat(var(--cols), 1fr);
.card {
height: 100%;
@@ -49,10 +45,18 @@
}
@include small-break {
--cols: 1;
--cols: 2;
&__wrap {
gap: var(--base);
}
&__card-list {
gap: base(0.4);
}
}
@include extra-small-break {
--cols: 1;
}
}

View File

@@ -1,7 +1,7 @@
import type { EntityToGroup } from '@payloadcms/ui/shared'
import type { AdminViewProps } from 'payload'
import { HydrateClientUser } from '@payloadcms/ui'
import { HydrateAuthProvider } from '@payloadcms/ui'
import { EntityType, RenderCustomComponent, groupNavItems } from '@payloadcms/ui/shared'
import LinkImport from 'next/link.js'
import React, { Fragment } from 'react'
@@ -79,7 +79,7 @@ export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult, params, se
return (
<Fragment>
<HydrateClientUser permissions={permissions} user={user} />
<HydrateAuthProvider permissions={permissions} />
<RenderCustomComponent
CustomComponent={
typeof CustomDashboardComponent === 'function' ? CustomDashboardComponent : undefined

View File

@@ -1,7 +1,11 @@
import type { AdminViewComponent, AdminViewProps, EditViewComponent } from 'payload'
import { DocumentInfoProvider, EditDepthProvider, HydrateClientUser } from '@payloadcms/ui'
import { RenderCustomComponent, formatAdminURL , isEditing as getIsEditing } from '@payloadcms/ui/shared'
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
import {
RenderCustomComponent,
formatAdminURL,
isEditing as getIsEditing,
} from '@payloadcms/ui/shared'
import { notFound, redirect } from 'next/navigation.js'
import React from 'react'
@@ -208,7 +212,15 @@ export const Document: React.FC<AdminViewProps> = async ({
permissions={permissions}
/>
)}
<HydrateClientUser permissions={permissions} user={user} />
<HydrateAuthProvider permissions={permissions} />
{/**
* After bumping the Next.js canary to 104, and React to 19.0.0-rc-06d0b89e-20240801" we have to deepCopy the permissions object (https://github.com/payloadcms/payload/pull/7541).
* If both HydrateClientUser and RenderCustomComponent receive the same permissions object (same object reference), we get a
* "TypeError: Cannot read properties of undefined (reading '$$typeof')" error when loading up some version views - for example a versions
* view in the draft-posts collection of the versions test suite. RenderCustomComponent is what renders the versions view.
*
* // TODO: Revisit this in the future and figure out why this is happening. Might be a React/Next.js bug. We don't know why it happens, and a future React/Next version might unbreak this (keep an eye on this and remove deepCopyObjectSimple if that's the case)
*/}
<EditDepthProvider
depth={1}
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}

View File

@@ -4,9 +4,7 @@ import {
Button,
CheckboxField,
ConfirmPasswordField,
EmailField,
PasswordField,
TextField,
useAuth,
useConfig,
useDocumentInfo,
@@ -14,12 +12,12 @@ import {
useFormModified,
useTranslation,
} from '@payloadcms/ui'
import { email as emailValidation } from 'payload/shared'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import type { Props } from './types.js'
import { RenderEmailAndUsernameFields } from '../../../../elements/EmailAndUsername/index.js'
import { APIKey } from './APIKey.js'
import './index.scss'
@@ -49,7 +47,7 @@ export const Auth: React.FC<Props> = (props) => {
const dispatchFields = useFormFields((reducer) => reducer[1])
const modified = useFormModified()
const { i18n, t } = useTranslation()
const { isInitializing } = useDocumentInfo()
const { docPermissions, isInitializing } = useDocumentInfo()
const {
routes: { api },
@@ -140,38 +138,12 @@ export const Auth: React.FC<Props> = (props) => {
<div className={[baseClass, className].filter(Boolean).join(' ')}>
{!disableLocalStrategy && (
<React.Fragment>
{Boolean(loginWithUsername) && (
<TextField
disabled={disabled}
label={t('authentication:username')}
name="username"
readOnly={readOnly}
required
/>
)}
{(!loginWithUsername ||
loginWithUsername?.allowEmailLogin ||
loginWithUsername?.requireEmail) && (
<EmailField
autoComplete="email"
disabled={disabled}
label={t('general:email')}
name="email"
readOnly={readOnly}
required={!loginWithUsername || loginWithUsername?.requireEmail}
validate={(value) =>
emailValidation(value, {
name: 'email',
type: 'email',
data: {},
preferences: { fields: {} },
req: { t } as any,
required: true,
siblingData: {},
})
}
/>
)}
<RenderEmailAndUsernameFields
loginWithUsername={loginWithUsername}
operation={operation}
permissions={docPermissions?.fields}
readOnly={readOnly}
/>
{(showPasswordFields || requirePassword) && (
<div className={`${baseClass}__changing-password`}>
<PasswordField
@@ -190,7 +162,7 @@ export const Auth: React.FC<Props> = (props) => {
buttonStyle="secondary"
disabled={disabled}
onClick={() => handleChangePassword(false)}
size="small"
size="medium"
>
{t('general:cancel')}
</Button>
@@ -201,7 +173,7 @@ export const Auth: React.FC<Props> = (props) => {
disabled={disabled}
id="change-password"
onClick={() => handleChangePassword(true)}
size="small"
size="medium"
>
{t('authentication:changePassword')}
</Button>
@@ -211,7 +183,7 @@ export const Auth: React.FC<Props> = (props) => {
buttonStyle="secondary"
disabled={disabled}
onClick={() => void unlock()}
size="small"
size="medium"
>
{t('authentication:forceUnlock')}
</Button>

View File

@@ -8,8 +8,8 @@
}
&__auth {
margin-bottom: calc(var(--base) * 2);
margin-top: calc(var(--base) * 0.5);
margin-bottom: base(1.6);
border-radius: var(--style-radius-s);
}
@include small-break {

View File

@@ -103,7 +103,15 @@ export const DefaultEditView: React.FC = () => {
const classes = [baseClass, id && `${baseClass}--is-editing`].filter(Boolean).join(' ')
const [schemaPath, setSchemaPath] = React.useState(entitySlug)
const [validateBeforeSubmit, setValidateBeforeSubmit] = useState(false)
const [validateBeforeSubmit, setValidateBeforeSubmit] = useState(() => {
if (
operation === 'create' &&
collectionConfig.auth &&
!collectionConfig.auth.disableLocalStrategy
)
return true
return false
})
const onSave = useCallback(
(json) => {

View File

@@ -51,7 +51,7 @@ export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult })
/>
</p>
<br />
<Button Link={Link} buttonStyle="secondary" el="link" to={adminRoute}>
<Button Link={Link} buttonStyle="secondary" el="link" size="large" to={adminRoute}>
{i18n.t('general:backToDashboard')}
</Button>
</Fragment>

View File

@@ -2,7 +2,6 @@
.collection-list {
width: 100%;
margin-top: base(0.5);
&__wrap {
padding-bottom: var(--spacing-view-bottom);
@@ -14,7 +13,7 @@
&__header {
display: flex;
align-items: flex-end;
align-items: center;
flex-wrap: wrap;
gap: base(0.75);
@@ -28,14 +27,12 @@
.pill {
position: relative;
top: -14px;
margin: 0;
}
}
&__sub-header {
flex-basis: 100%;
margin-top: base(0.25);
}
.table {
@@ -57,7 +54,7 @@
#heading-_select,
.cell-_select {
min-width: unset;
width: auto;
width: base(1);
}
}
}

View File

@@ -1,7 +1,7 @@
import type { AdminViewProps, Where } from 'payload'
import {
HydrateClientUser,
HydrateAuthProvider,
ListInfoProvider,
ListQueryProvider,
TableColumnsProvider,
@@ -57,9 +57,23 @@ export const ListView: React.FC<AdminViewProps> = async ({
req,
user,
where: {
key: {
equals: preferenceKey,
},
and: [
{
key: {
equals: preferenceKey,
},
},
{
'user.relationTo': {
equals: user.collection,
},
},
{
'user.value': {
equals: user?.id,
},
},
],
},
})
?.then((res) => res?.docs?.[0]?.value)) as ListPreferences
@@ -124,7 +138,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
return (
<Fragment>
<HydrateClientUser permissions={permissions} user={user} />
<HydrateAuthProvider permissions={permissions} />
<ListInfoProvider
collectionConfig={createClientCollectionConfig({
collection: collectionConfig,

View File

@@ -1,16 +1,16 @@
'use client'
import type { PayloadRequest } from 'payload'
import type { Validate, ValidateOptions } from 'payload'
import { EmailField, TextField, useConfig, useTranslation } from '@payloadcms/ui'
import { EmailField, TextField, useTranslation } from '@payloadcms/ui'
import { email, username } from 'payload/shared'
import React from 'react'
export type LoginFieldProps = {
required?: boolean
type: 'email' | 'emailOrUsername' | 'username'
validate?: Validate
}
export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true }) => {
const { t } = useTranslation()
const config = useConfig()
if (type === 'email') {
return (
@@ -20,17 +20,7 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true })
name="email"
path="email"
required={required}
validate={(value) =>
email(value, {
name: 'email',
type: 'email',
data: {},
preferences: { fields: {} },
req: { t } as PayloadRequest,
required: true,
siblingData: {},
})
}
validate={email}
/>
)
}
@@ -41,23 +31,8 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true })
label={t('authentication:username')}
name="username"
path="username"
required
validate={(value) =>
username(value, {
name: 'username',
type: 'text',
data: {},
preferences: { fields: {} },
req: {
payload: {
config,
},
t,
} as PayloadRequest,
required: true,
siblingData: {},
})
}
required={required}
validate={username}
/>
)
}
@@ -68,36 +43,13 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true })
label={t('authentication:emailOrUsername')}
name="username"
path="username"
required
validate={(value) => {
const passesUsername = username(value, {
name: 'username',
type: 'text',
data: {},
preferences: { fields: {} },
req: {
payload: {
config,
},
t,
} as PayloadRequest,
required: true,
siblingData: {},
})
const passesEmail = email(value, {
name: 'username',
type: 'email',
data: {},
preferences: { fields: {} },
req: {
payload: {
config,
},
t,
} as PayloadRequest,
required: true,
siblingData: {},
})
required={required}
validate={(value, options) => {
const passesUsername = username(value, options)
const passesEmail = email(
value,
options as ValidateOptions<any, { username?: string }, any, any>,
)
if (!passesEmail && !passesUsername) {
return `${t('general:email')}: ${passesEmail} ${t('general:username')}: ${passesUsername}`

View File

@@ -91,7 +91,7 @@ export const LoginForm: React.FC<{
>
{t('authentication:forgotPasswordQuestion')}
</Link>
<FormSubmit>{t('authentication:login')}</FormSubmit>
<FormSubmit size="large">{t('authentication:login')}</FormSubmit>
</Form>
)
}

View File

@@ -20,7 +20,7 @@ export const LogoutClient: React.FC<{
useEffect(() => {
if (!isLoggingOut) {
setIsLoggingOut(true)
logOut()
void logOut()
}
}, [isLoggingOut, logOut])
@@ -33,6 +33,7 @@ export const LogoutClient: React.FC<{
Link={Link}
buttonStyle="secondary"
el="link"
size="large"
url={formatAdminURL({
adminRoute,
path: `/login${

View File

@@ -38,7 +38,13 @@ export const NotFoundClient: React.FC<{
<Gutter className={`${baseClass}__wrap`}>
<h1>{t('general:nothingFound')}</h1>
<p>{t('general:sorryNotFound')}</p>
<Button Link={Link} className={`${baseClass}__button`} el="link" to={adminRoute}>
<Button
Link={Link}
className={`${baseClass}__button`}
el="link"
size="large"
to={adminRoute}
>
{t('general:backToDashboard')}
</Button>
</Gutter>

View File

@@ -2,7 +2,7 @@ import type { I18n } from '@payloadcms/translations'
import type { Metadata } from 'next'
import type { AdminViewComponent, SanitizedConfig } from 'payload'
import { HydrateClientUser } from '@payloadcms/ui'
import { HydrateAuthProvider } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared'
import React, { Fragment } from 'react'
@@ -58,21 +58,18 @@ export const NotFoundPage = async ({
})
return (
<Fragment>
<HydrateClientUser permissions={initPageResult.permissions} user={initPageResult.req.user} />
<DefaultTemplate
i18n={initPageResult.req.i18n}
locale={initPageResult.locale}
params={params}
payload={initPageResult.req.payload}
permissions={initPageResult.permissions}
searchParams={searchParams}
user={initPageResult.req.user}
visibleEntities={initPageResult.visibleEntities}
>
<NotFoundClient />
</DefaultTemplate>
</Fragment>
<DefaultTemplate
i18n={initPageResult.req.i18n}
locale={initPageResult.locale}
params={params}
payload={initPageResult.req.payload}
permissions={initPageResult.permissions}
searchParams={searchParams}
user={initPageResult.req.user}
visibleEntities={initPageResult.visibleEntities}
>
<NotFoundClient />
</DefaultTemplate>
)
}

View File

@@ -81,7 +81,7 @@ export const ResetPasswordClient: React.FC<Args> = ({ token }) => {
/>
<ConfirmPasswordField />
<HiddenField forceUsePathFromProps name="token" value={token} />
<FormSubmit>{i18n.t('authentication:resetPassword')}</FormSubmit>
<FormSubmit size="large">{i18n.t('authentication:resetPassword')}</FormSubmit>
</Form>
)
}

View File

@@ -30,7 +30,13 @@ export const UnauthorizedView: AdminViewComponent = ({ initPageResult }) => {
<Gutter className={baseClass}>
<h2>{i18n.t('error:unauthorized')}</h2>
<p>{i18n.t('error:notAllowedToAccessPage')}</p>
<Button Link={Link} className={`${baseClass}__button`} el="link" to={logoutRoute}>
<Button
Link={Link}
className={`${baseClass}__button`}
el="link"
size="large"
to={logoutRoute}
>
{i18n.t('authentication:logOut')}
</Button>
</Gutter>

View File

@@ -81,7 +81,7 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
const canUpdate = docPermissions?.update?.permission
const localeValues = locales.map((locale) => locale.value)
const localeValues = locales && locales.map((locale) => locale.value)
return (
<main className={baseClass}>

View File

@@ -26,7 +26,13 @@ const generateLabelFromValue = (
locale: string,
value: { relationTo: string; value: RelationshipValue } | RelationshipValue,
): string => {
let relation: string
if (Array.isArray(value)) {
return value
.map((v) => generateLabelFromValue(collections, field, locale, v))
.filter(Boolean) // Filters out any undefined or empty values
.join(', ')
}
let relatedDoc: RelationshipValue
let valueToReturn = '' as any
@@ -37,17 +43,20 @@ const generateLabelFromValue = (
return String(value)
}
if (Array.isArray(relationTo)) {
if (typeof value === 'object') {
relation = value.relationTo
relatedDoc = value.value
}
if (typeof value === 'object' && 'relationTo' in value) {
relatedDoc = value.value
} else {
relation = relationTo
// Non-polymorphic relationship
relatedDoc = value
}
const relatedCollection = collections.find((c) => c.slug === relation)
const relatedCollection = relationTo
? collections.find(
(c) =>
c.slug ===
(typeof value === 'object' && 'relationTo' in value ? value.relationTo : relationTo),
)
: null
if (relatedCollection) {
const useAsTitle = relatedCollection?.admin?.useAsTitle
@@ -56,45 +65,65 @@ const generateLabelFromValue = (
)
let titleFieldIsLocalized = false
if (useAsTitleField && fieldAffectsData(useAsTitleField))
if (useAsTitleField && fieldAffectsData(useAsTitleField)) {
titleFieldIsLocalized = useAsTitleField.localized
}
if (typeof relatedDoc?.[useAsTitle] !== 'undefined') {
valueToReturn = relatedDoc[useAsTitle]
} else if (typeof relatedDoc?.id !== 'undefined') {
valueToReturn = relatedDoc.id
} else {
valueToReturn = relatedDoc
}
if (typeof valueToReturn === 'object' && titleFieldIsLocalized) {
valueToReturn = valueToReturn[locale]
}
} else if (relatedDoc) {
// Handle non-polymorphic `hasMany` relationships or fallback
if (typeof relatedDoc?.id !== 'undefined') {
valueToReturn = relatedDoc.id
} else {
valueToReturn = relatedDoc
}
}
if (typeof valueToReturn === 'object' && valueToReturn !== null) {
valueToReturn = JSON.stringify(valueToReturn)
}
return valueToReturn
}
const Relationship: React.FC<Props> = ({ comparison, field, i18n, locale, version }) => {
let placeholder = ''
const placeholder = `[${i18n.t('general:noValue')}]`
const { collections } = useConfig()
if (version === comparison) placeholder = `[${i18n.t('general:noValue')}]`
let versionToRender: string | undefined = placeholder
let comparisonToRender: string | undefined = placeholder
let versionToRender = version
let comparisonToRender = comparison
if (version) {
if ('hasMany' in field && field.hasMany && Array.isArray(version)) {
versionToRender =
version.map((val) => generateLabelFromValue(collections, field, locale, val)).join(', ') ||
placeholder
} else {
versionToRender = generateLabelFromValue(collections, field, locale, version) || placeholder
}
}
if ('hasMany' in field && field.hasMany) {
if (Array.isArray(version))
versionToRender = version
.map((val) => generateLabelFromValue(collections, field, locale, val))
.join(', ')
if (Array.isArray(comparison))
comparisonToRender = comparison
.map((val) => generateLabelFromValue(collections, field, locale, val))
.join(', ')
} else {
versionToRender = generateLabelFromValue(collections, field, locale, version)
comparisonToRender = generateLabelFromValue(collections, field, locale, comparison)
if (comparison) {
if ('hasMany' in field && field.hasMany && Array.isArray(comparison)) {
comparisonToRender =
comparison
.map((val) => generateLabelFromValue(collections, field, locale, val))
.join(', ') || placeholder
} else {
comparisonToRender =
generateLabelFromValue(collections, field, locale, comparison) || placeholder
}
}
const label =
@@ -112,10 +141,8 @@ const Relationship: React.FC<Props> = ({ comparison, field, i18n, locale, versio
</Label>
<ReactDiffViewer
hideLineNumbers
newValue={typeof versionToRender !== 'undefined' ? String(versionToRender) : placeholder}
oldValue={
typeof comparisonToRender !== 'undefined' ? String(comparisonToRender) : placeholder
}
newValue={versionToRender}
oldValue={comparisonToRender}
showDiffOnly={false}
splitView
styles={diffStyles}

View File

@@ -18,12 +18,12 @@ export default {
number: Text,
point: Text,
radio: Select,
relationship: null,
relationship: Relationship,
richText: Text,
row: Nested,
select: Select,
tabs: Tabs,
text: Text,
textarea: Text,
upload: null,
upload: Relationship,
}

View File

@@ -1,12 +1,12 @@
import type {
CollectionPermission,
Document,
EditViewComponent,
GlobalPermission,
OptionObject,
} from 'payload'
import { notFound } from 'next/navigation.js'
import {
type CollectionPermission,
type Document,
type EditViewComponent,
type GlobalPermission,
type OptionObject,
deepCopyObjectSimple,
} from 'payload'
import React from 'react'
import { getLatestVersion } from '../Versions/getLatestVersion.js'
@@ -55,8 +55,18 @@ export const VersionView: EditViewComponent = async (props) => {
})
if (collectionConfig?.versions?.drafts) {
latestDraftVersion = await getLatestVersion(payload, slug, 'draft', 'collection')
latestPublishedVersion = await getLatestVersion(payload, slug, 'published', 'collection')
latestDraftVersion = await getLatestVersion({
slug,
type: 'collection',
payload,
status: 'draft',
})
latestPublishedVersion = await getLatestVersion({
slug,
type: 'collection',
payload,
status: 'published',
})
}
} catch (error) {
return notFound()
@@ -80,8 +90,18 @@ export const VersionView: EditViewComponent = async (props) => {
})
if (globalConfig?.versions?.drafts) {
latestDraftVersion = await getLatestVersion(payload, slug, 'draft', 'global')
latestPublishedVersion = await getLatestVersion(payload, slug, 'published', 'global')
latestDraftVersion = await getLatestVersion({
slug,
type: 'global',
payload,
status: 'draft',
})
latestPublishedVersion = await getLatestVersion({
slug,
type: 'global',
payload,
status: 'published',
})
}
} catch (error) {
return notFound()
@@ -116,7 +136,14 @@ export const VersionView: EditViewComponent = async (props) => {
return (
<DefaultVersionView
doc={doc}
docPermissions={docPermissions}
/**
* After bumping the Next.js canary to 104, and React to 19.0.0-rc-06d0b89e-20240801" we have to deepCopy the permissions object (https://github.com/payloadcms/payload/pull/7541).
* If both HydrateClientUser and RenderCustomComponent receive the same permissions object (same object reference), we get a
* "TypeError: Cannot read properties of undefined (reading '$$typeof')" error
*
* // TODO: Revisit this in the future and figure out why this is happening. Might be a React/Next.js bug. We don't know why it happens, and a future React/Next version might unbreak this (keep an eye on this and remove deepCopyObjectSimple if that's the case)
*/
docPermissions={deepCopyObjectSimple(docPermissions)}
initialComparisonDoc={latestVersion}
latestDraftVersion={latestDraftVersion?.id}
latestPublishedVersion={latestPublishedVersion?.id}

View File

@@ -1,4 +1,19 @@
export async function getLatestVersion(payload, slug, status, type = 'collection') {
import type { Payload } from 'payload'
type ReturnType = {
id: string
updatedAt: string
} | null
type Args = {
payload: Payload
slug: string
status: 'draft' | 'published'
type: 'collection' | 'global'
}
export async function getLatestVersion(args: Args): Promise<ReturnType> {
const { slug, type = 'collection', payload, status } = args
try {
const sharedOptions = {
depth: 0,
@@ -22,11 +37,16 @@ export async function getLatestVersion(payload, slug, status, type = 'collection
...sharedOptions,
})
if (!response.docs.length) {
return null
}
return {
id: response.docs[0].id,
updatedAt: response.docs[0].updatedAt,
}
} catch (e) {
console.error(e)
return null
}
}

View File

@@ -62,13 +62,18 @@ export const VersionsView: EditViewComponent = async (props) => {
},
})
if (collectionConfig?.versions?.drafts) {
latestDraftVersion = await getLatestVersion(payload, collectionSlug, 'draft', 'collection')
latestPublishedVersion = await getLatestVersion(
latestDraftVersion = await getLatestVersion({
slug: collectionSlug,
type: 'collection',
payload,
collectionSlug,
'published',
'collection',
)
status: 'draft',
})
latestPublishedVersion = await getLatestVersion({
slug: collectionSlug,
type: 'collection',
payload,
status: 'published',
})
}
} catch (error) {
console.error(error) // eslint-disable-line no-console
@@ -90,8 +95,18 @@ export const VersionsView: EditViewComponent = async (props) => {
})
if (globalConfig?.versions?.drafts) {
latestDraftVersion = await getLatestVersion(payload, globalSlug, 'draft', 'global')
latestPublishedVersion = await getLatestVersion(payload, globalSlug, 'published', 'global')
latestDraftVersion = await getLatestVersion({
slug: globalSlug,
type: 'global',
payload,
status: 'draft',
})
latestPublishedVersion = await getLatestVersion({
slug: globalSlug,
type: 'global',
payload,
status: 'published',
})
}
} catch (error) {
console.error(error) // eslint-disable-line no-console

View File

@@ -18,16 +18,20 @@ export const withPayload = (nextConfig = {}) => {
env: {
...(nextConfig?.env || {}),
},
outputFileTracingExcludes: {
...(nextConfig?.outputFileTracingExcludes || {}),
'**/*': [
...(nextConfig?.outputFileTracingExcludes?.['**/*'] || []),
'drizzle-kit',
'drizzle-kit/api',
],
},
outputFileTracingIncludes: {
...(nextConfig?.outputFileTracingIncludes || {}),
'**/*': [...(nextConfig?.outputFileTracingIncludes?.['**/*'] || []), '@libsql/client'],
},
experimental: {
...(nextConfig?.experimental || {}),
outputFileTracingExcludes: {
'**/*': [
...(nextConfig.experimental?.outputFileTracingExcludes?.['**/*'] || []),
'drizzle-kit',
'drizzle-kit/payload',
'libsql',
],
},
turbo: {
...(nextConfig?.experimental?.turbo || {}),
resolveAlias: {
@@ -63,9 +67,9 @@ export const withPayload = (nextConfig = {}) => {
serverExternalPackages: [
...(nextConfig?.serverExternalPackages || []),
'drizzle-kit',
'drizzle-kit/payload',
'libsql',
'drizzle-kit/api',
'pino',
'libsql',
'pino-pretty',
'graphql',
],
@@ -80,7 +84,7 @@ export const withPayload = (nextConfig = {}) => {
externals: [
...(incomingWebpackConfig?.externals || []),
'drizzle-kit',
'drizzle-kit/payload',
'drizzle-kit/api',
'sharp',
'libsql',
],

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.75",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",
@@ -84,7 +84,7 @@
"pretest": "pnpm build"
},
"dependencies": {
"@next/env": "^15.0.0-canary.53",
"@next/env": "^15.0.0-canary.104",
"@payloadcms/translations": "workspace:*",
"@swc-node/core": "1.13.1",
"@swc-node/sourcemap-support": "0.5.0",

View File

@@ -6,7 +6,7 @@ import type { SanitizedCollectionConfig, TypeWithID } from '../collections/confi
import type { SanitizedConfig } from '../config/types.js'
import type { Field, FieldAffectingData, RichTextField, Validate } from '../fields/config/types.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import type { JsonObject, PayloadRequest, RequestContext } from '../types/index.js'
import type { JsonObject, Payload, PayloadRequest, RequestContext } from '../types/index.js'
import type { WithServerSidePropsComponentProps } from './elements/WithServerSideProps.js'
export type RichTextFieldProps<Value extends object, AdapterProps, ExtraFieldProperties = {}> = {
@@ -189,6 +189,7 @@ type RichTextAdapterBase<
WithServerSideProps: React.FC<Omit<WithServerSidePropsComponentProps, 'serverOnlyProps'>>
config: SanitizedConfig
i18n: I18nClient
payload: Payload
schemaPath: string
}) => Map<string, React.ReactNode>
generateSchemaMap?: (args: {

View File

@@ -1,4 +1,5 @@
import type { ArrayField } from '../../fields/config/types.js'
import type { ArrayFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { FieldMap } from '../forms/FieldMap.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -12,8 +13,9 @@ export type ArrayFieldProps = {
maxRows?: ArrayField['maxRows']
minRows?: ArrayField['minRows']
name?: string
validate?: ArrayFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type ArrayFieldLabelComponent = LabelComponent<'array'>

View File

@@ -1,4 +1,5 @@
import type { Block, BlockField } from '../../fields/config/types.js'
import type { BlockFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { FieldMap } from '../forms/FieldMap.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -12,8 +13,9 @@ export type BlocksFieldProps = {
minRows?: number
name?: string
slug?: string
validate?: BlockFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type ReducedBlock = {
LabelComponent: Block['admin']['components']['Label']

View File

@@ -1,3 +1,4 @@
import type { CheckboxFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -9,8 +10,9 @@ export type CheckboxFieldProps = {
onChange?: (val: boolean) => void
partialChecked?: boolean
path?: string
validate?: CheckboxFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type CheckboxFieldLabelComponent = LabelComponent<'checkbox'>

View File

@@ -1,4 +1,5 @@
import type { CodeField } from '../../fields/config/types.js'
import type { CodeFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -7,8 +8,9 @@ export type CodeFieldProps = {
language?: CodeField['admin']['language']
name?: string
path?: string
validate?: CodeFieldValidation
width: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type CodeFieldLabelComponent = LabelComponent<'code'>

View File

@@ -1,4 +1,5 @@
import type { DateField } from '../../fields/config/types.js'
import type { DateFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -7,8 +8,9 @@ export type DateFieldProps = {
name?: string
path?: string
placeholder?: DateField['admin']['placeholder'] | string
validate?: DateFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type DateFieldLabelComponent = LabelComponent<'date'>

View File

@@ -1,4 +1,5 @@
import type { EmailField } from '../../fields/config/types.js'
import type { EmailFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -7,8 +8,9 @@ export type EmailFieldProps = {
name?: string
path?: string
placeholder?: EmailField['admin']['placeholder']
validate?: EmailFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type EmailFieldLabelComponent = LabelComponent<'email'>

View File

@@ -1,4 +1,5 @@
import type { JSONField } from '../../fields/config/types.js'
import type { JSONFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -7,8 +8,9 @@ export type JSONFieldProps = {
jsonSchema?: Record<string, unknown>
name?: string
path?: string
validate?: JSONFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type JSONFieldLabelComponent = LabelComponent<'json'>

View File

@@ -1,4 +1,5 @@
import type { NumberField } from '../../fields/config/types.js'
import type { NumberFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -12,8 +13,9 @@ export type NumberFieldProps = {
path?: string
placeholder?: NumberField['admin']['placeholder']
step?: number
validate?: NumberFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type NumberFieldLabelComponent = LabelComponent<'number'>

View File

@@ -1,3 +1,4 @@
import type { PointFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -6,8 +7,9 @@ export type PointFieldProps = {
path?: string
placeholder?: string
step?: number
validate?: PointFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type PointFieldLabelComponent = LabelComponent<'point'>

View File

@@ -1,4 +1,5 @@
import type { Option } from '../../fields/config/types.js'
import type { RadioFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -8,9 +9,10 @@ export type RadioFieldProps = {
onChange?: OnChange
options?: Option[]
path?: string
validate?: RadioFieldValidation
value?: string
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type OnChange<T = string> = (value: T) => void

View File

@@ -1,4 +1,5 @@
import type { RelationshipField } from '../../fields/config/types.js'
import type { RelationshipFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -9,8 +10,9 @@ export type RelationshipFieldProps = {
name: string
relationTo?: RelationshipField['relationTo']
sortOptions?: RelationshipField['admin']['sortOptions']
validate?: RelationshipFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type RelationshipFieldLabelComponent = LabelComponent<'relationship'>

View File

@@ -1,3 +1,4 @@
import type { RichTextFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { MappedField } from '../forms/FieldMap.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -5,8 +6,9 @@ import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../typ
export type RichTextComponentProps = {
name: string
richTextComponentMap?: Map<string, MappedField[] | React.ReactNode>
validate?: RichTextFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type RichTextFieldLabelComponent = LabelComponent<'richText'>

View File

@@ -1,4 +1,5 @@
import type { Option } from '../../fields/config/types.js'
import type { SelectFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -10,9 +11,10 @@ export type SelectFieldProps = {
onChange?: (e: string | string[]) => void
options?: Option[]
path?: string
validate?: SelectFieldValidation
value?: string
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type SelectFieldLabelComponent = LabelComponent<'select'>

View File

@@ -1,4 +1,5 @@
import type { TextField } from '../../fields/config/types.js'
import type { TextFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -13,8 +14,9 @@ export type TextFieldProps = {
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
path?: string
placeholder?: TextField['admin']['placeholder']
validate?: TextFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type TextFieldLabelComponent = LabelComponent<'text'>

View File

@@ -1,4 +1,5 @@
import type { TextareaField } from '../../fields/config/types.js'
import type { TextareaFieldValidation } from '../../fields/validations.js'
import type { ErrorComponent } from '../forms/Error.js'
import type { DescriptionComponent, FormFieldBase, LabelComponent } from '../types.js'
@@ -9,8 +10,9 @@ export type TextareaFieldProps = {
path?: string
placeholder?: TextareaField['admin']['placeholder']
rows?: number
validate?: TextareaFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type TextareaFieldLabelComponent = LabelComponent<'textarea'>

View File

@@ -1,4 +1,10 @@
import type { DescriptionComponent, FormFieldBase, LabelComponent, UploadField } from 'payload'
import type {
DescriptionComponent,
FormFieldBase,
LabelComponent,
UploadField,
UploadFieldValidation,
} from 'payload'
import type { ErrorComponent } from '../forms/Error.js'
@@ -7,8 +13,9 @@ export type UploadFieldProps = {
name?: string
path?: string
relationTo?: UploadField['relationTo']
validate?: UploadFieldValidation
width?: string
} & FormFieldBase
} & Omit<FormFieldBase, 'validate'>
export type UploadFieldLabelComponent = LabelComponent<'upload'>

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