Compare commits
20 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b9397399a | ||
|
|
cdcc35ccdb | ||
|
|
442189ec48 | ||
|
|
5d1cc760c9 | ||
|
|
2f90683c7d | ||
|
|
3f5403a52a | ||
|
|
9bccdfd60a | ||
|
|
62666a9897 | ||
|
|
eb27b84854 | ||
|
|
c3480811d3 | ||
|
|
12ba820de4 | ||
|
|
95fcd13929 | ||
|
|
6141c5950b | ||
|
|
0040e1756c | ||
|
|
1ebd54b315 | ||
|
|
cdb2072a6d | ||
|
|
68553ff974 | ||
|
|
9a3bce1118 | ||
|
|
005befcbe2 | ||
|
|
e65b6478c9 |
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -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}",
|
||||
|
||||
@@ -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'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'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'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'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`.
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Stripe Plugin
|
||||
label: Stripe
|
||||
order: 20
|
||||
order: 40
|
||||
desc: Easily accept payments with Stripe
|
||||
keywords: plugins, stripe, payments, ecommerce
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.73",
|
||||
"version": "3.0.0-beta.74",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)')
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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, '')
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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, '')
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -10,6 +10,6 @@ export const requireDrizzleKit: RequireDrizzleKit = () => {
|
||||
const {
|
||||
generateSQLiteDrizzleJson: generateDrizzleJson,
|
||||
pushSQLiteSchema: pushSchema,
|
||||
} = require('drizzle-kit/payload')
|
||||
} = require('drizzle-kit/api')
|
||||
return { generateDrizzleJson, pushSchema }
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
103
packages/next/src/elements/EmailAndUsername/index.tsx
Normal file
103
packages/next/src/elements/EmailAndUsername/index.tsx
Normal 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=""
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
59
packages/next/src/scss/toastify.scss
Normal file
59
packages/next/src/scss/toastify.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,3 +3,7 @@
|
||||
margin-bottom: var(--base);
|
||||
}
|
||||
}
|
||||
|
||||
.emailAndUsername {
|
||||
margin-bottom: var(--base);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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${
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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'>
|
||||
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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'>
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
|
||||
@@ -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'>
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
77
packages/payload/src/auth/ensureUsernameOrEmail.ts
Normal file
77
packages/payload/src/auth/ensureUsernameOrEmail.ts
Normal 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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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?: {
|
||||
|
||||
@@ -66,4 +66,5 @@ export const authDefaults: IncomingAuthType = {
|
||||
export const loginWithUsernameDefaults: LoginWithUsernameOptions = {
|
||||
allowEmailLogin: false,
|
||||
requireEmail: false,
|
||||
requireUsername: true,
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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[]>([])
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user