Compare commits

..

20 Commits

Author SHA1 Message Date
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
293 changed files with 3435 additions and 1846 deletions

7
.vscode/launch.json vendored
View File

@@ -56,6 +56,13 @@
"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.74",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.74",
"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.74",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.74",
"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",
"drizzle-orm": "0.32.1",
"pg": "8.11.3",
"prompts": "2.4.2",

View File

@@ -1,4 +1,4 @@
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
import type { CreateMigration } from 'payload'
import fs from 'fs'
@@ -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 } = require('drizzle-kit/api')
const drizzleJsonAfter = generateDrizzleJson(this.schema)
const [yyymmdd, hhmmss] = new Date().toISOString().split('T')
const formattedDate = yyymmdd.replace(/\D/g, '')

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

@@ -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,

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.74",
"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",
"drizzle-orm": "0.32.1",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",

View File

@@ -1,4 +1,4 @@
import type { DrizzleSnapshotJSON } from 'drizzle-kit/payload'
import type { DrizzleSnapshotJSON } from 'drizzle-kit/api'
import type { CreateMigration } from 'payload'
import fs from 'fs'
@@ -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, '')

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

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

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.74",
"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.74",
"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.74",
"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.74",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.74",
"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.74",
"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.74",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

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', 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

@@ -5,7 +5,6 @@ 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 React from 'react'
@@ -16,14 +15,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',
@@ -100,7 +91,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}

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,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,10 +57,12 @@ 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
label={t('authentication:newPassword')}
name="password"
@@ -77,7 +78,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

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

@@ -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,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

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

@@ -24,8 +24,13 @@ export const withPayload = (nextConfig = {}) => {
'**/*': [
...(nextConfig.experimental?.outputFileTracingExcludes?.['**/*'] || []),
'drizzle-kit',
'drizzle-kit/payload',
'libsql',
'drizzle-kit/api',
],
},
outputFileTracingIncludes: {
'**/*': [
...(nextConfig.experimental?.outputFileTracingIncludes?.['**/*'] || []),
'@libsql/client',
],
},
turbo: {
@@ -63,9 +68,9 @@ export const withPayload = (nextConfig = {}) => {
serverExternalPackages: [
...(nextConfig?.serverExternalPackages || []),
'drizzle-kit',
'drizzle-kit/payload',
'libsql',
'drizzle-kit/api',
'pino',
'libsql',
'pino-pretty',
'graphql',
],
@@ -80,7 +85,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.74",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",

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

View File

@@ -1,4 +1,4 @@
import type { ClientValidate, Field } from '../../fields/config/types.js'
import type { Field, Validate } from '../../fields/config/types.js'
import type { Where } from '../../types/index.js'
export type Data = {
@@ -25,7 +25,7 @@ export type FormField = {
passesCondition?: boolean
rows?: Row[]
valid: boolean
validate?: ClientValidate
validate?: Validate
value: unknown
}

View File

@@ -1,8 +1,8 @@
import type { Field } from '../../fields/config/types.js'
import type { EmailField } from '../../fields/config/types.js'
import { email } from '../../fields/validations.js'
export const emailField = ({ required = true }: { required?: boolean }): Field => ({
export const emailFieldConfig: EmailField = {
name: 'email',
type: 'email',
admin: {
@@ -20,7 +20,7 @@ export const emailField = ({ required = true }: { required?: boolean }): Field =
],
},
label: ({ t }) => t('general:email'),
required,
required: true,
unique: true,
validate: email,
})
}

View File

@@ -1,8 +1,8 @@
import type { Field } from '../../fields/config/types.js'
import type { TextField } from '../../fields/config/types.js'
import { username } from '../../fields/validations.js'
export const usernameField: Field = {
export const usernameFieldConfig: TextField = {
name: 'username',
type: 'text',
admin: {

View File

@@ -0,0 +1,77 @@
import type { RequiredDataFromCollectionSlug } from '../collections/config/types.js'
import type { AuthCollection, CollectionSlug, PayloadRequest } from '../index.js'
import { ValidationError } from '../errors/index.js'
type ValidateUsernameOrEmailArgs<TSlug extends CollectionSlug> = {
authOptions: AuthCollection['config']['auth']
collectionSlug: string
data: RequiredDataFromCollectionSlug<TSlug>
req: PayloadRequest
} & (
| {
operation: 'create'
originalDoc?: never
}
| {
operation: 'update'
originalDoc: RequiredDataFromCollectionSlug<TSlug>
}
)
export const ensureUsernameOrEmail = <TSlug extends CollectionSlug>({
authOptions: { disableLocalStrategy, loginWithUsername },
collectionSlug,
data,
operation,
originalDoc,
req,
}: ValidateUsernameOrEmailArgs<TSlug>) => {
// neither username or email are required
// and neither are provided
// so we need to manually validate
if (
!disableLocalStrategy &&
loginWithUsername &&
!loginWithUsername.requireEmail &&
!loginWithUsername.requireUsername
) {
let missingFields = false
if (operation === 'create' && !data.email && !data.username) {
missingFields = true
} else if (operation === 'update') {
// prevent clearing both email and username
if ('email' in data && !data.email && 'username' in data && !data.username) {
missingFields = true
}
// prevent clearing email if no username
if ('email' in data && !data.email && !originalDoc.username) {
missingFields = true
}
// prevent clearing username if no email
if ('username' in data && !data.username && !originalDoc.email) {
missingFields = true
}
}
if (missingFields) {
throw new ValidationError(
{
collection: collectionSlug,
errors: [
{
field: 'username',
message: 'Username or email is required',
},
{
field: 'email',
message: 'Username or email is required',
},
],
},
req.t,
)
}
}
return
}

View File

@@ -1,11 +1,11 @@
import type { Field } from '../fields/config/types.js'
import type { Field, TextField } from '../fields/config/types.js'
import type { IncomingAuthType } from './types.js'
import { accountLockFields } from './baseFields/accountLock.js'
import { apiKeyFields } from './baseFields/apiKey.js'
import { baseAuthFields } from './baseFields/auth.js'
import { emailField } from './baseFields/email.js'
import { usernameField } from './baseFields/username.js'
import { emailFieldConfig } from './baseFields/email.js'
import { usernameFieldConfig } from './baseFields/username.js'
import { verificationFields } from './baseFields/verification.js'
export const getBaseAuthFields = (authConfig: IncomingAuthType): Field[] => {
@@ -16,19 +16,24 @@ export const getBaseAuthFields = (authConfig: IncomingAuthType): Field[] => {
}
if (!authConfig.disableLocalStrategy) {
const emailFieldIndex = authFields.push(emailField({ required: true })) - 1
const emailField = { ...emailFieldConfig }
let usernameField: TextField | undefined
if (authConfig.loginWithUsername) {
if (
typeof authConfig.loginWithUsername === 'object' &&
authConfig.loginWithUsername.requireEmail === false
) {
authFields[emailFieldIndex] = emailField({ required: false })
usernameField = { ...usernameFieldConfig }
if (typeof authConfig.loginWithUsername === 'object') {
if (authConfig.loginWithUsername.requireEmail === false) {
emailField.required = false
}
if (authConfig.loginWithUsername.requireUsername === false) {
usernameField.required = false
}
}
authFields.push(usernameField)
}
authFields.push(emailField)
if (usernameField) authFields.push(usernameField)
authFields.push(...baseAuthFields)
if (authConfig.verify) {

View File

@@ -11,6 +11,7 @@ import { Forbidden } from '../../errors/index.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { ensureUsernameOrEmail } from '../ensureUsernameOrEmail.js'
export type Arguments<TSlug extends CollectionSlug> = {
collection: Collection
@@ -44,6 +45,14 @@ export const registerFirstUserOperation = async <TSlug extends CollectionSlug>(
try {
const shouldCommit = await initTransaction(req)
ensureUsernameOrEmail<TSlug>({
authOptions: config.auth,
collectionSlug: slug,
data,
operation: 'create',
req,
})
const doc = await payload.db.findOne({
collection: config.slug,
req,

View File

@@ -118,10 +118,18 @@ export type AuthStrategy = {
name: string
}
export type LoginWithUsernameOptions = {
allowEmailLogin?: boolean
requireEmail?: boolean
}
export type LoginWithUsernameOptions =
| {
allowEmailLogin?: false
requireEmail?: boolean
// If `allowEmailLogin` is false, `requireUsername` must be true (default: true)
requireUsername?: true
}
| {
allowEmailLogin?: true
requireEmail?: boolean
requireUsername?: boolean
}
export interface IncomingAuthType {
cookies?: {

View File

@@ -66,4 +66,5 @@ export const authDefaults: IncomingAuthType = {
export const loginWithUsernameDefaults: LoginWithUsernameOptions = {
allowEmailLogin: false,
requireEmail: false,
requireUsername: true,
}

View File

@@ -1,3 +1,4 @@
import type { LoginWithUsernameOptions } from '../../auth/types.js'
import type { Config, SanitizedConfig } from '../../config/types.js'
import type { CollectionConfig, SanitizedCollectionConfig } from './types.js'
@@ -153,14 +154,24 @@ export const sanitizeCollection = async (
sanitized.auth.strategies = []
}
sanitized.auth.loginWithUsername = sanitized.auth.loginWithUsername
? {
if (sanitized.auth.loginWithUsername) {
if (sanitized.auth.loginWithUsername === true) {
sanitized.auth.loginWithUsername = loginWithUsernameDefaults
} else {
const loginWithUsernameWithDefaults = {
...loginWithUsernameDefaults,
...(typeof sanitized.auth.loginWithUsername === 'boolean'
? {}
: sanitized.auth.loginWithUsername),
...sanitized.auth.loginWithUsername,
} as LoginWithUsernameOptions
// if allowEmailLogin is false, requireUsername must be true
if (loginWithUsernameWithDefaults.allowEmailLogin === false) {
loginWithUsernameWithDefaults.requireUsername = true
}
: false
sanitized.auth.loginWithUsername = loginWithUsernameWithDefaults
}
} else {
sanitized.auth.loginWithUsername = false
}
sanitized.fields = mergeBaseFields(sanitized.fields, getBaseAuthFields(sanitized.auth))
}

View File

@@ -11,6 +11,7 @@ import type {
RequiredDataFromCollectionSlug,
} from '../config/types.js'
import { ensureUsernameOrEmail } from '../../auth/ensureUsernameOrEmail.js'
import executeAccess from '../../auth/executeAccess.js'
import { sendVerificationEmail } from '../../auth/sendVerificationEmail.js'
import { registerLocalStrategy } from '../../auth/strategies/local/register.js'
@@ -49,6 +50,14 @@ export const createOperation = async <TSlug extends CollectionSlug>(
try {
const shouldCommit = await initTransaction(args.req)
ensureUsernameOrEmail<TSlug>({
authOptions: args.collection.config.auth,
collectionSlug: args.collection.config.slug,
data: args.data,
operation: 'create',
req: args.req,
})
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////

View File

@@ -12,6 +12,7 @@ import type {
RequiredDataFromCollectionSlug,
} from '../config/types.js'
import { ensureUsernameOrEmail } from '../../auth/ensureUsernameOrEmail.js'
import executeAccess from '../../auth/executeAccess.js'
import { combineQueries } from '../../database/combineQueries.js'
import { validateQueryPaths } from '../../database/queryValidation/validateQueryPaths.js'
@@ -199,6 +200,17 @@ export const updateOperation = async <TSlug extends CollectionSlug>(
req,
})
if (args.collection.config.auth) {
ensureUsernameOrEmail<TSlug>({
authOptions: args.collection.config.auth,
collectionSlug: args.collection.config.slug,
data: args.data,
operation: 'update',
originalDoc,
req: args.req,
})
}
// /////////////////////////////////////
// beforeValidate - Fields
// /////////////////////////////////////

View File

@@ -11,6 +11,7 @@ import type {
RequiredDataFromCollectionSlug,
} from '../config/types.js'
import { ensureUsernameOrEmail } from '../../auth/ensureUsernameOrEmail.js'
import executeAccess from '../../auth/executeAccess.js'
import { generatePasswordSaltHash } from '../../auth/strategies/local/generatePasswordSaltHash.js'
import { hasWhereAccessResult } from '../../auth/types.js'
@@ -143,6 +144,17 @@ export const updateByIDOperation = async <TSlug extends CollectionSlug>(
showHiddenFields: true,
})
if (args.collection.config.auth) {
ensureUsernameOrEmail<TSlug>({
authOptions: args.collection.config.auth,
collectionSlug: args.collection.config.slug,
data: args.data,
operation: 'update',
originalDoc,
req: args.req,
})
}
// /////////////////////////////////////
// Generate data for all files and sizes
// /////////////////////////////////////

View File

@@ -8,13 +8,20 @@ import { APIError } from './APIError.js'
// This gets dynamically reassigned during compilation
export let ValidationErrorName = 'ValidationError'
export type ValidationFieldError = {
// The field path, i.e. "textField", "groupField.subTextField", etc.
field: string
// The error message to display for this field
message: string
}
export class ValidationError extends APIError<{
collection?: string
errors: { field: string; message: string }[]
errors: ValidationFieldError[]
global?: string
}> {
constructor(
results: { collection?: string; errors: { field: string; message: string }[]; global?: string },
results: { collection?: string; errors: ValidationFieldError[]; global?: string },
t?: TFunction,
) {
const message = t

View File

@@ -20,3 +20,4 @@ export { NotFound } from './NotFound.js'
export { QueryError } from './QueryError.js'
export { ReservedFieldName } from './ReservedFieldName.js'
export { ValidationError, ValidationErrorName } from './ValidationError.js'
export type { ValidationFieldError } from './ValidationError.js'

View File

@@ -7,6 +7,7 @@ import type { CSSProperties } from 'react'
import monacoeditor from 'monaco-editor' // IMPORTANT - DO NOT REMOVE: This is required for pnpm's default isolated mode to work - even though the import is not used. This is due to a typescript bug: https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1519138189. (tsbugisolatedmode)
import type { JSONSchema4 } from 'json-schema'
import type React from 'react'
import type { DeepPartial } from 'ts-essentials'
import type { RichTextAdapter, RichTextAdapterProvider } from '../../admin/RichText.js'
import type { ErrorComponent } from '../../admin/forms/Error.js'
@@ -176,12 +177,14 @@ export type Labels = {
}
export type BaseValidateOptions<TData, TSiblingData, TValue> = {
collectionSlug?: string
data: Partial<TData>
id?: number | string
operation?: Operation
preferences: DocumentPreferences
previousValue?: TValue
req: PayloadRequest
required?: boolean
siblingData: Partial<TSiblingData>
}
@@ -202,8 +205,6 @@ export type Validate<
options: ValidateOptions<TData, TSiblingData, TFieldConfig, TValue>,
) => Promise<string | true> | string | true
export type ClientValidate = Omit<Validate, 'req'>
export type OptionObject = {
label: LabelFunction | LabelStatic
value: string

View File

@@ -136,6 +136,7 @@ export const promise = async ({
const validationResult = await field.validate(valueToValidate, {
...field,
id,
collectionSlug: collection?.slug,
data: deepMergeWithSourceArrays(doc, data),
jsonError,
operation,

View File

@@ -31,7 +31,8 @@ import type {
import { isNumber } from '../utilities/isNumber.js'
import { isValidID } from '../utilities/isValidID.js'
export const text: Validate<string | string[], unknown, unknown, TextField> = (
export type TextFieldValidation = Validate<string, unknown, unknown, TextField>
export const text: TextFieldValidation = (
value,
{
hasMany,
@@ -83,7 +84,8 @@ export const text: Validate<string | string[], unknown, unknown, TextField> = (
return true
}
export const password: Validate<string, unknown, unknown, TextField> = (
export type PasswordFieldValidation = Validate<string, unknown, unknown, TextField>
export const password: PasswordFieldValidation = (
value,
{
maxLength: fieldMaxLength,
@@ -115,32 +117,54 @@ export const password: Validate<string, unknown, unknown, TextField> = (
return true
}
export const confirmPassword: Validate<string, unknown, unknown, TextField> = (
export type ConfirmPasswordFieldValidation = Validate<
string,
unknown,
{ password: string },
TextField
>
export const confirmPassword: ConfirmPasswordFieldValidation = (
value,
{ req: { data, t }, required },
{ req: { t }, required, siblingData },
) => {
if (required && !value) {
return t('validation:required')
}
if (
value &&
typeof data.formState === 'object' &&
'password' in data.formState &&
typeof data.formState.password === 'object' &&
'value' in data.formState.password &&
value !== data.formState.password.value
) {
if (value && value !== siblingData.password) {
return t('fields:passwordsDoNotMatch')
}
return true
}
export const email: Validate<string, unknown, unknown, EmailField> = (
export type EmailFieldValidation = Validate<string, unknown, { username?: string }, EmailField>
export const email: EmailFieldValidation = (
value,
{ req: { t }, required },
{
collectionSlug,
req: {
payload: { config },
t,
},
required,
siblingData,
},
) => {
if (collectionSlug) {
const collection = config.collections.find(({ slug }) => slug === collectionSlug)
if (
collection.auth.loginWithUsername &&
!collection.auth.loginWithUsername?.requireUsername &&
!collection.auth.loginWithUsername?.requireEmail
) {
if (!value && !siblingData?.username) {
return t('validation:required')
}
}
}
if ((value && !/\S[^\s@]*@\S+\.\S+/.test(value)) || (!value && required)) {
return t('validation:emailAddress')
}
@@ -148,18 +172,35 @@ export const email: Validate<string, unknown, unknown, EmailField> = (
return true
}
export const username: Validate<string, unknown, unknown, TextField> = (
export type UsernameFieldValidation = Validate<string, unknown, { email?: string }, TextField>
export const username: UsernameFieldValidation = (
value,
{
collectionSlug,
req: {
payload: { config },
t,
},
required,
siblingData,
},
) => {
let maxLength: number
if (collectionSlug) {
const collection = config.collections.find(({ slug }) => slug === collectionSlug)
if (
collection.auth.loginWithUsername &&
!collection.auth.loginWithUsername?.requireUsername &&
!collection.auth.loginWithUsername?.requireEmail
) {
if (!value && !siblingData?.email) {
return t('validation:required')
}
}
}
if (typeof config?.defaultMaxTextLength === 'number') maxLength = config.defaultMaxTextLength
if (value && maxLength && value.length > maxLength) {
@@ -173,7 +214,8 @@ export const username: Validate<string, unknown, unknown, TextField> = (
return true
}
export const textarea: Validate<string, unknown, unknown, TextareaField> = (
export type TextareaFieldValidation = Validate<string, unknown, unknown, TextareaField>
export const textarea: TextareaFieldValidation = (
value,
{
maxLength: fieldMaxLength,
@@ -204,10 +246,8 @@ export const textarea: Validate<string, unknown, unknown, TextareaField> = (
return true
}
export const code: Validate<string, unknown, unknown, CodeField> = (
value,
{ req: { t }, required },
) => {
export type CodeFieldValidation = Validate<string, unknown, unknown, CodeField>
export const code: CodeFieldValidation = (value, { req: { t }, required }) => {
if (required && value === undefined) {
return t('validation:required')
}
@@ -215,7 +255,13 @@ export const code: Validate<string, unknown, unknown, CodeField> = (
return true
}
export const json: Validate<string, unknown, unknown, { jsonError?: string } & JSONField> = async (
export type JSONFieldValidation = Validate<
string,
unknown,
unknown,
{ jsonError?: string } & JSONField
>
export const json: JSONFieldValidation = async (
value,
{ jsonError, jsonSchema, req: { t }, required },
) => {
@@ -281,10 +327,8 @@ export const json: Validate<string, unknown, unknown, { jsonError?: string } & J
return true
}
export const checkbox: Validate<boolean, unknown, unknown, CheckboxField> = (
value,
{ req: { t }, required },
) => {
export type CheckboxFieldValidation = Validate<boolean, unknown, unknown, CheckboxField>
export const checkbox: CheckboxFieldValidation = (value, { req: { t }, required }) => {
if ((value && typeof value !== 'boolean') || (required && typeof value !== 'boolean')) {
return t('validation:trueOrFalse')
}
@@ -292,10 +336,8 @@ export const checkbox: Validate<boolean, unknown, unknown, CheckboxField> = (
return true
}
export const date: Validate<Date, unknown, unknown, DateField> = (
value,
{ req: { t }, required },
) => {
export type DateFieldValidation = Validate<Date, unknown, unknown, DateField>
export const date: DateFieldValidation = (value, { req: { t }, required }) => {
if (value && !isNaN(Date.parse(value.toString()))) {
return true
}
@@ -311,10 +353,8 @@ export const date: Validate<Date, unknown, unknown, DateField> = (
return true
}
export const richText: Validate<object, unknown, unknown, RichTextField> = async (
value,
options,
) => {
export type RichTextFieldValidation = Validate<object, unknown, unknown, RichTextField>
export const richText: RichTextFieldValidation = async (value, options) => {
if (!options?.editor) {
throw new Error('richText field has no editor property.')
}
@@ -357,7 +397,8 @@ const validateArrayLength = (
return true
}
export const number: Validate<number | number[], unknown, unknown, NumberField> = (
export type NumberFieldValidation = Validate<number | number[], unknown, unknown, NumberField>
export const number: NumberFieldValidation = (
value,
{ hasMany, max, maxRows, min, minRows, req: { t }, required },
) => {
@@ -391,17 +432,13 @@ export const number: Validate<number | number[], unknown, unknown, NumberField>
return true
}
export const array: Validate<unknown[], unknown, unknown, ArrayField> = (
value,
{ maxRows, minRows, req: { t }, required },
) => {
export type ArrayFieldValidation = Validate<unknown[], unknown, unknown, ArrayField>
export const array: ArrayFieldValidation = (value, { maxRows, minRows, req: { t }, required }) => {
return validateArrayLength(value, { maxRows, minRows, required, t })
}
export const blocks: Validate<unknown, unknown, unknown, BlockField> = (
value,
{ maxRows, minRows, req: { t }, required },
) => {
export type BlockFieldValidation = Validate<unknown, unknown, unknown, BlockField>
export const blocks: BlockFieldValidation = (value, { maxRows, minRows, req: { t }, required }) => {
return validateArrayLength(value, { maxRows, minRows, required, t })
}
@@ -533,10 +570,8 @@ const validateFilterOptions: Validate<
return true
}
export const upload: Validate<unknown, unknown, unknown, UploadField> = (
value: string,
options,
) => {
export type UploadFieldValidation = Validate<unknown, unknown, unknown, UploadField>
export const upload: UploadFieldValidation = (value: string, options) => {
if (!value && options.required) {
return options?.req?.t('validation:required')
}
@@ -554,12 +589,13 @@ export const upload: Validate<unknown, unknown, unknown, UploadField> = (
return validateFilterOptions(value, options)
}
export const relationship: Validate<
export type RelationshipFieldValidation = Validate<
RelationshipValue,
unknown,
unknown,
RelationshipField
> = async (value, options) => {
>
export const relationship: RelationshipFieldValidation = async (value, options) => {
const {
maxRows,
minRows,
@@ -634,7 +670,8 @@ export const relationship: Validate<
return validateFilterOptions(value, options)
}
export const select: Validate<unknown, unknown, unknown, SelectField> = (
export type SelectFieldValidation = Validate<string | string[], unknown, unknown, SelectField>
export const select: SelectFieldValidation = (
value,
{ hasMany, options, req: { t }, required },
) => {
@@ -671,10 +708,8 @@ export const select: Validate<unknown, unknown, unknown, SelectField> = (
return true
}
export const radio: Validate<unknown, unknown, unknown, RadioField> = (
value,
{ options, req: { t }, required },
) => {
export type RadioFieldValidation = Validate<unknown, unknown, unknown, RadioField>
export const radio: RadioFieldValidation = (value, { options, req: { t }, required }) => {
if (value) {
const valueMatchesOption = options.some(
(option) => option === value || (typeof option !== 'string' && option.value === value),
@@ -685,10 +720,13 @@ export const radio: Validate<unknown, unknown, unknown, RadioField> = (
return required ? t('validation:required') : true
}
export const point: Validate<[number | string, number | string], unknown, unknown, PointField> = (
value = ['', ''],
{ req: { t }, required },
) => {
export type PointFieldValidation = Validate<
[number | string, number | string],
unknown,
unknown,
PointField
>
export const point: PointFieldValidation = (value = ['', ''], { req: { t }, required }) => {
const lng = parseFloat(String(value[0]))
const lat = parseFloat(String(value[1]))
if (

View File

@@ -870,7 +870,6 @@ export type {
Block,
BlockField,
CheckboxField,
ClientValidate,
CodeField,
CollapsibleField,
Condition,
@@ -928,6 +927,27 @@ export { traverseFields as afterReadTraverseFields } from './fields/hooks/afterR
export { traverseFields as beforeChangeTraverseFields } from './fields/hooks/beforeChange/traverseFields.js'
export { traverseFields as beforeValidateTraverseFields } from './fields/hooks/beforeValidate/traverseFields.js'
export { default as sortableFieldTypes } from './fields/sortableFieldTypes.js'
export type {
ArrayFieldValidation,
BlockFieldValidation,
CheckboxFieldValidation,
CodeFieldValidation,
ConfirmPasswordFieldValidation,
DateFieldValidation,
EmailFieldValidation,
JSONFieldValidation,
NumberFieldValidation,
PasswordFieldValidation,
PointFieldValidation,
RadioFieldValidation,
RelationshipFieldValidation,
RichTextFieldValidation,
SelectFieldValidation,
TextFieldValidation,
TextareaFieldValidation,
UploadFieldValidation,
UsernameFieldValidation,
} from './fields/validations.js'
export type { ClientGlobalConfig } from './globals/config/client.js'
export { createClientGlobalConfig } from './globals/config/client.js'
export type {
@@ -999,7 +1019,8 @@ export { deleteCollectionVersions } from './versions/deleteCollectionVersions.js
export { enforceMaxVersions } from './versions/enforceMaxVersions.js'
export { getLatestCollectionVersion } from './versions/getLatestCollectionVersion.js'
export { getLatestGlobalVersion } from './versions/getLatestGlobalVersion.js'
export { saveVersion } from './versions/saveVersion.js'
export { getDependencies }
export { saveVersion } from './versions/saveVersion.js'
export type { TypeWithVersion } from './versions/types.js'
export { deepMergeSimple } from '@payloadcms/translations/utilities'

View File

@@ -619,24 +619,6 @@ const generateAuthFieldTypes = ({
loginWithUsername: Auth['loginWithUsername']
type: 'forgotOrUnlock' | 'login' | 'register'
}): JSONSchema4 => {
const emailAuthFields = {
additionalProperties: false,
properties: { email: fieldType },
required: ['email'],
}
const usernameAuthFields = {
additionalProperties: false,
properties: { username: fieldType },
required: ['username'],
}
if (['login', 'register'].includes(type)) {
emailAuthFields.properties['password'] = fieldType
emailAuthFields.required.push('password')
usernameAuthFields.properties['password'] = fieldType
usernameAuthFields.required.push('password')
}
if (loginWithUsername) {
switch (type) {
case 'login': {
@@ -644,38 +626,57 @@ const generateAuthFieldTypes = ({
// allow username or email and require password for login
return {
additionalProperties: false,
oneOf: [emailAuthFields, usernameAuthFields],
oneOf: [
{
additionalProperties: false,
properties: { email: fieldType, password: fieldType },
required: ['email', 'password'],
},
{
additionalProperties: false,
properties: { password: fieldType, username: fieldType },
required: ['username', 'password'],
},
],
}
} else {
// allow only username and password for login
return usernameAuthFields
return {
additionalProperties: false,
properties: {
password: fieldType,
username: fieldType,
},
required: ['username', 'password'],
}
}
}
case 'register': {
const requiredFields: ('email' | 'password' | 'username')[] = ['password']
const properties: {
email?: JSONSchema4['properties']
password?: JSONSchema4['properties']
username?: JSONSchema4['properties']
} = {
password: fieldType,
username: fieldType,
}
if (loginWithUsername.requireEmail) {
// require username, email and password for registration
return {
additionalProperties: false,
properties: {
...usernameAuthFields.properties,
...emailAuthFields.properties,
},
required: [...usernameAuthFields.required, ...emailAuthFields.required],
}
} else if (loginWithUsername.allowEmailLogin) {
// allow both but only require username for registration
return {
additionalProperties: false,
properties: {
...usernameAuthFields.properties,
...emailAuthFields.properties,
},
required: usernameAuthFields.required,
}
} else {
// require only username and password for registration
return usernameAuthFields
requiredFields.push('email')
}
if (loginWithUsername.requireUsername) {
requiredFields.push('username')
}
if (loginWithUsername.requireEmail || loginWithUsername.allowEmailLogin) {
properties.email = fieldType
}
return {
additionalProperties: false,
properties,
required: requiredFields,
}
}
@@ -684,18 +685,37 @@ const generateAuthFieldTypes = ({
// allow email or username for unlock/forgot-password
return {
additionalProperties: false,
oneOf: [emailAuthFields, usernameAuthFields],
oneOf: [
{
additionalProperties: false,
properties: { email: fieldType },
required: ['email'],
},
{
additionalProperties: false,
properties: { username: fieldType },
required: ['username'],
},
],
}
} else {
// allow only username for unlock/forgot-password
return usernameAuthFields
return {
additionalProperties: false,
properties: { username: fieldType },
required: ['username'],
}
}
}
}
}
// default email (and password for login/register)
return emailAuthFields
return {
additionalProperties: false,
properties: { email: fieldType, password: fieldType },
required: ['email', 'password'],
}
}
export function authCollectionToOperationsJSONSchema(

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud-storage",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.74",
"description": "The official cloud storage plugin for Payload CMS",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.74",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.74",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",

View File

@@ -1,13 +1,15 @@
'use client'
import type { TextFieldProps } from 'payload'
import type { SelectFieldValidation, TextFieldProps } from 'payload'
import { SelectField, useForm } from '@payloadcms/ui'
import React, { useEffect, useState } from 'react'
import type { SelectFieldOption } from '../../types.js'
export const DynamicFieldSelector: React.FC<TextFieldProps> = (props) => {
export const DynamicFieldSelector: React.FC<
{ validate: SelectFieldValidation } & TextFieldProps
> = (props) => {
const { fields, getDataByPath } = useForm()
const [options, setOptions] = useState<SelectFieldOption[]>([])

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.74",
"description": "The official Nested Docs plugin for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-redirects",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.74",
"description": "Redirects plugin for Payload",
"keywords": [
"payload",

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