Compare commits

..

9 Commits

Author SHA1 Message Date
Paul Popus
c2cd6bbaec fix test error 2024-07-29 17:04:22 -04:00
Paul Popus
b2657ed12b fix tests 2024-07-29 14:02:28 -04:00
Paul Popus
0851ef99da update tests 2024-07-29 13:31:22 -04:00
Paul Popus
3c483ab57a update tests 2024-07-29 12:35:11 -04:00
Paul Popus
9bdbbb674f capitalise letters 2024-07-29 10:58:20 -04:00
Paul Popus
eb191f335b dont uppercase 2024-07-26 13:52:32 -04:00
Paul Popus
02bb176890 dont capitalise 2024-07-26 13:45:18 -04:00
Paul Popus
082c6ef44a add test and cleanup 2024-07-25 18:53:05 -04:00
Paul Popus
f93cdf9364 feat: format the error for field schema 2024-07-25 18:13:27 -04:00
419 changed files with 3783 additions and 5074 deletions

View File

@@ -196,7 +196,6 @@ jobs:
- postgres-custom-schema
- postgres-uuid
- supabase
- sqlite
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@@ -549,45 +548,3 @@ jobs:
steps:
- if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
run: exit 1
publish-canary:
name: Publish Canary
if: github.ref == 'refs/heads/beta'
runs-on: ubuntu-latest
needs:
- all-green
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
run: sudo ethtool -K eth0 tx off rx off
- name: Setup Node@${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: ${{ env.PNPM_VERSION }}
run_install: false
- name: Restore build
uses: actions/cache@v4
timeout-minutes: 10
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Load npm token
run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Canary release script
# dry run hard-coded to true for testing and no npm token provided
run: pnpm tsx ./scripts/publish-canary.ts
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true

View File

@@ -38,7 +38,6 @@ jobs:
db-\*
db-mongodb
db-postgres
db-sqlite
email-nodemailer
eslint
graphql

View File

@@ -266,10 +266,12 @@ export const myField: Field = {
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
Custom Label Components receive all [Field Component](#the-field-component) props, plus the following props:
All Label Components receive the following props:
| Property | Description |
| -------------- | ---------------------------------------------------------------- |
| **`label`** | Label value provided in field, it can be used with i18n. |
| **`required`** | The `admin.required` property defined in the [Field Config](../fields/overview). |
| **`schemaPath`** | The path to the field in the schema. Similar to `path`, but without dynamic indices. |
<Banner type="success">
@@ -277,36 +279,6 @@ Custom Label Components receive all [Field Component](#the-field-component) prop
All [Custom Server Components](./components) receive the `payload` and `i18n` properties by default. See [Building Custom Components](./components#building-custom-components) for more details.
</Banner>
#### TypeScript
When building Custom Error Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Error Component, one for every [Field Type](../fields/overview). The convention is to append `ErrorComponent` to the type of field, i.e. `TextFieldErrorComponent`.
```tsx
import type {
ArrayFieldLabelComponent,
BlocksFieldLabelComponent,
CheckboxFieldLabelComponent,
CodeFieldLabelComponent,
CollapsibleFieldLabelComponent,
DateFieldLabelComponent,
EmailFieldLabelComponent,
GroupFieldLabelComponent,
HiddenFieldLabelComponent,
JSONFieldLabelComponent,
NumberFieldLabelComponent,
PointFieldLabelComponent,
RadioFieldLabelComponent,
RelationshipFieldLabelComponent,
RichTextFieldLabelComponent,
RowFieldLabelComponent,
SelectFieldLabelComponent,
TabsFieldLabelComponent,
TextFieldLabelComponent,
TextareaFieldLabelComponent,
UploadFieldLabelComponent
} from 'payload'
```
### The Error Component
The Error Component is rendered when a field fails validation. It is typically displayed beneath the field input in a visually-compelling style.
@@ -329,7 +301,7 @@ export const myField: Field = {
_For details on how to build Custom Components, see [Building Custom Components](./components#building-custom-components)._
Custom Error Components receive all [Field Component](#the-field-component) props, plus the following props:
All Error Components receive the following props:
| Property | Description |
| --------------- | ------------------------------------------------------------- |
@@ -340,36 +312,6 @@ Custom Error Components receive all [Field Component](#the-field-component) prop
All [Custom Server Components](./components) receive the `payload` and `i18n` properties by default. See [Building Custom Components](./components#building-custom-components) for more details.
</Banner>
#### TypeScript
When building Custom Error Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Error Component, one for every [Field Type](../fields/overview). The convention is to append `ErrorComponent` to the type of field, i.e. `TextFieldErrorComponent`.
```tsx
import type {
ArrayFieldErrorComponent,
BlocksFieldErrorComponent,
CheckboxFieldErrorComponent,
CodeFieldErrorComponent,
CollapsibleFieldErrorComponent,
DateFieldErrorComponent,
EmailFieldErrorComponent,
GroupFieldErrorComponent,
HiddenFieldErrorComponent,
JSONFieldErrorComponent,
NumberFieldErrorComponent,
PointFieldErrorComponent,
RadioFieldErrorComponent,
RelationshipFieldErrorComponent,
RichTextFieldErrorComponent,
RowFieldErrorComponent,
SelectFieldErrorComponent,
TabsFieldErrorComponent,
TextFieldErrorComponent,
TextareaFieldErrorComponent,
UploadFieldErrorComponent
} from 'payload'
```
### The Description Property
Field Descriptions are used to provide additional information to the editor about a field, such as special instructions. Their placement varies from field to field, but typically are displayed with subtle style differences beneath the field inputs.
@@ -464,7 +406,7 @@ export const MyCollectionConfig: SanitizedCollectionConfig = {
_For details on how to build a Custom Description, see [Building Custom Components](./components#building-custom-components)._
Custom Description Components receive all [Field Component](#the-field-component) props, plus the following props:
All Description Components receive the following props:
| Property | Description |
| -------------- | ---------------------------------------------------------------- |
@@ -475,36 +417,6 @@ Custom Description Components receive all [Field Component](#the-field-component
All [Custom Server Components](./components) receive the `payload` and `i18n` properties by default. See [Building Custom Components](./components#building-custom-components) for more details.
</Banner>
#### TypeScript
When building Custom Description Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Description Component, one for every [Field Type](../fields/overview). The convention is to append `DescriptionComponent` to the type of field, i.e. `TextFieldDescriptionComponent`.
```tsx
import type {
ArrayFieldDescriptionComponent,
BlocksFieldDescriptionComponent,
CheckboxFieldDescriptionComponent,
CodeFieldDescriptionComponent,
CollapsibleFieldDescriptionComponent,
DateFieldDescriptionComponent,
EmailFieldDescriptionComponent,
GroupFieldDescriptionComponent,
HiddenFieldDescriptionComponent,
JSONFieldDescriptionComponent,
NumberFieldDescriptionComponent,
PointFieldDescriptionComponent,
RadioFieldDescriptionComponent,
RelationshipFieldDescriptionComponent,
RichTextFieldDescriptionComponent,
RowFieldDescriptionComponent,
SelectFieldDescriptionComponent,
TabsFieldDescriptionComponent,
TextFieldDescriptionComponent,
TextareaFieldDescriptionComponent,
UploadFieldDescriptionComponent
} from 'payload'
```
### afterInput and beforeInput
With these properties you can add multiple components _before_ and _after_ the input element, as their name suggests. This is useful when you need to render additional elements alongside the field without replacing the entire field component.

View File

@@ -205,9 +205,7 @@ export const MyField: Field = {
}
```
Default values can be defined as a static value or a function that returns a value. When a `defaultValue` is defined statically, Payload's DB adapters will apply it to the database schema or models.
Functions can be written to make use of the following argument properties:
Default values can be defined as a static string or a function that returns a string. Functions are called with the following arguments:
- `user` - the authenticated user object
- `locale` - the currently selected locale string
@@ -296,7 +294,7 @@ When using custom validation functions, Payload will use yours in place of the d
To reuse default field validations, call them from within your custom validation function:
```ts
import { text } from 'payload/shared'
import { text } from 'payload/fields/validations'
const field: Field = {
name: 'notBad',

View File

@@ -193,7 +193,7 @@ You can learn more about writing queries [here](/docs/queries/overview).
When a relationship field has both <strong>filterOptions</strong> and a custom{' '}
<strong>validate</strong> function, the api will not validate <strong>filterOptions</strong>{' '}
unless you call the default relationship field validation function imported from{' '}
<strong>payload/shared</strong> in your validate function.
<strong>payload/fields/validations</strong> in your validate function.
</Banner>
## How the data is saved

View File

@@ -57,7 +57,6 @@ export const MyUploadField: Field = {
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. |
| **`defaultValue`** | Provide data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
| **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More](/docs/upload/overview#collection-upload-options). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`admin`** | Admin-specific configuration. [Admin Options](../admin/fields#admin-options). |
@@ -124,5 +123,5 @@ You can learn more about writing queries [here](/docs/queries/overview).
When an upload field has both <strong>filterOptions</strong> and a custom{' '}
<strong>validate</strong> function, the api will not validate <strong>filterOptions</strong>{' '}
unless you call the default upload field validation function imported from{' '}
<strong>payload/shared</strong> in your validate function.
<strong>payload/fields/validations</strong> in your validate function.
</Banner>

View File

@@ -335,6 +335,7 @@ import {
FieldMap,
File,
Form,
FormFieldBase,
FormLoadingOverlayToggle,
FormSubmit,
GenerateConfirmation,

View File

@@ -94,7 +94,6 @@ _An asterisk denotes that an option is required._
| **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](../admin/overview). Crop is enabled by default. [More](#crop-and-focal-point-selector) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
| **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/fields/upload#config-options). |
| **`externalFileHeaderFilter`** | Accepts existing headers and returns the headers after filtering or modifying. |
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
| **`focalPoint`** | Set to `false` to disable the focal point selection tool in the [Admin Panel](../admin/overview). The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) |

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.68",
"private": true,
"type": "module",
"scripts": {
@@ -119,7 +119,7 @@
"create-payload-app": "workspace:*",
"cross-env": "7.0.3",
"dotenv": "16.4.5",
"drizzle-orm": "0.32.1",
"drizzle-orm": "0.29.4",
"escape-html": "^1.0.3",
"execa": "5.1.1",
"form-data": "3.0.1",
@@ -150,7 +150,7 @@
"tempy": "1.0.1",
"tsx": "4.16.2",
"turbo": "^1.13.3",
"typescript": "5.5.4"
"typescript": "5.5.3"
},
"peerDependencies": {
"react": "^19.0.0 || ^19.0.0-rc-6230622a1a-20240610",

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.68",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -50,7 +50,6 @@
"dependencies": {
"@clack/prompts": "^0.7.0",
"@sindresorhus/slugify": "^1.1.0",
"@swc/core": "^1.6.13",
"arg": "^5.0.0",
"chalk": "^4.1.0",
"comment-json": "^4.2.3",

View File

@@ -79,7 +79,7 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const installSpinner = p.spinner()
installSpinner.start('Installing Payload and dependencies...')
const configurationResult = await installAndConfigurePayload({
const configurationResult = installAndConfigurePayload({
...args,
nextAppDetails,
nextConfigType,
@@ -143,16 +143,15 @@ async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean)
}
}
async function installAndConfigurePayload(
function installAndConfigurePayload(
args: {
nextAppDetails: NextAppDetails
nextConfigType: NextConfigType
useDistFiles?: boolean
} & InitNextArgs,
): Promise<
):
| { payloadConfigPath: string; success: true }
| { payloadConfigPath?: string; reason: string; success: false }
> {
| { payloadConfigPath?: string; reason: string; success: false } {
const {
'--debug': debug,
nextAppDetails: { isSrcDir, nextAppDir, nextConfigPath } = {},
@@ -213,7 +212,7 @@ async function installAndConfigurePayload(
copyRecursiveSync(templateSrcDir, path.dirname(nextConfigPath), debug)
// Wrap next.config.js with withPayload
await wrapNextConfig({ nextConfigPath, nextConfigType })
wrapNextConfig({ nextConfigPath, nextConfigType })
return {
payloadConfigPath: path.resolve(nextAppDir, '../payload.config.ts'),
@@ -240,63 +239,25 @@ async function installDeps(projectDir: string, packageManager: PackageManager, d
export async function getNextAppDetails(projectDir: string): Promise<NextAppDetails> {
const isSrcDir = fs.existsSync(path.resolve(projectDir, 'src'))
// Match next.config.js, next.config.ts, next.config.mjs, next.config.cjs
const nextConfigPath: string | undefined = (
await globby('next.config.(\\w)?(t|j)s', { absolute: true, cwd: projectDir })
await globby('next.config.*js', { absolute: true, cwd: projectDir })
)?.[0]
if (!nextConfigPath || nextConfigPath.length === 0) {
return {
hasTopLevelLayout: false,
isSrcDir,
isSupportedNextVersion: false,
nextConfigPath: undefined,
nextVersion: null,
}
}
const packageObj = await fse.readJson(path.resolve(projectDir, 'package.json'))
// Check if Next.js version is new enough
let nextVersion = null
if (packageObj.dependencies?.next) {
nextVersion = packageObj.dependencies.next
// Match versions using regex matching groups
const versionMatch = /(?<major>\d+)/.exec(nextVersion)
if (!versionMatch) {
p.log.warn(`Could not determine Next.js version from ${nextVersion}`)
return {
hasTopLevelLayout: false,
isSrcDir,
isSupportedNextVersion: false,
nextConfigPath,
nextVersion,
}
}
const { major } = versionMatch.groups as { major: string }
const majorVersion = parseInt(major)
if (majorVersion < 15) {
return {
hasTopLevelLayout: false,
isSrcDir,
isSupportedNextVersion: false,
nextConfigPath,
nextVersion,
}
}
}
const isSupportedNextVersion = true
// Check if Payload already installed
if (packageObj.dependencies?.payload) {
return {
hasTopLevelLayout: false,
isPayloadInstalled: true,
isSrcDir,
isSupportedNextVersion,
nextConfigPath,
nextVersion,
}
}
@@ -319,27 +280,14 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
? fs.existsSync(path.resolve(nextAppDir, 'layout.tsx'))
: false
return {
hasTopLevelLayout,
isSrcDir,
isSupportedNextVersion,
nextAppDir,
nextConfigPath,
nextConfigType: configType,
nextVersion,
}
return { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigPath, nextConfigType: configType }
}
function getProjectType(args: {
nextConfigPath: string
packageObj: Record<string, unknown>
}): NextConfigType {
}): 'cjs' | 'esm' {
const { nextConfigPath, packageObj } = args
if (nextConfigPath.endsWith('.ts')) {
return 'ts'
}
if (nextConfigPath.endsWith('.mjs')) {
return 'esm'
}

View File

@@ -29,22 +29,9 @@ const postgresReplacement: DbAdapterReplacement = {
packageName: '@payloadcms/db-postgres',
}
const sqliteReplacement: DbAdapterReplacement = {
configReplacement: (envName = 'DATABASE_URI') => [
' db: sqliteAdapter({',
' client: {',
` url: process.env.${envName} || '',`,
' },',
' }),',
],
importReplacement: "import { sqliteAdapter } from '@payloadcms/db-sqlite'",
packageName: '@payloadcms/db-sqlite',
}
export const dbReplacements: Record<DbType, DbAdapterReplacement> = {
mongodb: mongodbReplacement,
postgres: postgresReplacement,
sqlite: sqliteReplacement,
}
type StorageAdapterReplacement = {

View File

@@ -5,7 +5,6 @@ import type { CliArgs, DbDetails, DbType } from '../types.js'
type DbChoice = {
dbConnectionPrefix: `${string}/`
dbConnectionSuffix?: string
title: string
value: DbType
}
@@ -21,12 +20,6 @@ const dbChoiceRecord: Record<DbType, DbChoice> = {
title: 'PostgreSQL (beta)',
value: 'postgres',
},
sqlite: {
dbConnectionPrefix: 'file:./',
dbConnectionSuffix: '.db',
title: 'SQLite (beta)',
value: 'sqlite',
},
}
export async function selectDb(args: CliArgs, projectName: string): Promise<DbDetails> {
@@ -44,10 +37,10 @@ export async function selectDb(args: CliArgs, projectName: string): Promise<DbDe
dbType = await p.select<{ label: string; value: DbType }[], DbType>({
initialValue: 'mongodb',
message: `Select a database`,
options: Object.values(dbChoiceRecord).map((dbChoice) => ({
label: dbChoice.title,
value: dbChoice.value,
})),
options: [
{ label: 'MongoDB', value: 'mongodb' },
{ label: 'Postgres', value: 'postgres' },
],
})
if (p.isCancel(dbType)) process.exit(0)
}
@@ -57,7 +50,7 @@ export async function selectDb(args: CliArgs, projectName: string): Promise<DbDe
let dbUri: string | symbol | undefined = undefined
const initialDbUri = `${dbChoice.dbConnectionPrefix}${
projectName === '.' ? `payload-${getRandomDigitSuffix()}` : slugify(projectName)
}${dbChoice.dbConnectionSuffix || ''}`
}`
if (args['--db-accept-recommended']) {
dbUri = initialDbUri

View File

@@ -3,35 +3,6 @@ import { jest } from '@jest/globals'
import { parseAndModifyConfigContent, withPayloadStatement } from './wrap-next-config.js'
const tsConfigs = {
defaultNextConfig: `import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
export default nextConfig;`,
nextConfigExportNamedDefault: `import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
const wrapped = someFunc(asdf);
export { wrapped as default };
`,
nextConfigWithFunc: `import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
export default someFunc(nextConfig);
`,
nextConfigWithFuncMultiline: `import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
export default someFunc(
nextConfig
);
`,
nextConfigWithSpread: `import type { NextConfig } from "next";
const nextConfig: NextConfig = {
...someConfig,
};
export default nextConfig;
`,
}
const esmConfigs = {
defaultNextConfig: `/** @type {import('next').NextConfig} */
const nextConfig = {};
@@ -81,66 +52,27 @@ module.exports = nextConfig;
}
describe('parseAndInsertWithPayload', () => {
describe('ts', () => {
const configType = 'ts'
const importStatement = withPayloadStatement[configType]
it('should parse the default next config', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
tsConfigs.defaultNextConfig,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse the config with a function', async () => {
const { modifiedConfigContent: modifiedConfigContent2 } = await parseAndModifyConfigContent(
tsConfigs.nextConfigWithFunc,
configType,
)
expect(modifiedConfigContent2).toContain('withPayload(someFunc(nextConfig))')
})
it('should parse the config with a multi-lined function', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
tsConfigs.nextConfigWithFuncMultiline,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n {2}nextConfig\n\)\)/)
})
it('should parse the config with a spread', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
tsConfigs.nextConfigWithSpread,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
})
describe('esm', () => {
const configType = 'esm'
const importStatement = withPayloadStatement[configType]
it('should parse the default next config', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.defaultNextConfig,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse the config with a function', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.nextConfigWithFunc,
configType,
)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
})
it('should parse the config with a multi-lined function', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.nextConfigWithFuncMultiline,
configType,
)
@@ -148,8 +80,8 @@ describe('parseAndInsertWithPayload', () => {
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n {2}nextConfig\n\)\)/)
})
it('should parse the config with a spread', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
it('should parse the config with a spread', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
esmConfigs.nextConfigWithSpread,
configType,
)
@@ -158,10 +90,10 @@ describe('parseAndInsertWithPayload', () => {
})
// Unsupported: export { wrapped as default }
it('should give warning with a named export as default', async () => {
it('should give warning with a named export as default', () => {
const warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {})
const { modifiedConfigContent, success } = await parseAndModifyConfigContent(
const { modifiedConfigContent, success } = parseAndModifyConfigContent(
esmConfigs.nextConfigExportNamedDefault,
configType,
)
@@ -177,39 +109,39 @@ describe('parseAndInsertWithPayload', () => {
describe('cjs', () => {
const configType = 'cjs'
const requireStatement = withPayloadStatement[configType]
it('should parse the default next config', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.defaultNextConfig,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse anonymous default config', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
it('should parse anonymous default config', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.anonConfig,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload({})')
})
it('should parse the config with a function', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigWithFunc,
configType,
)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
})
it('should parse the config with a multi-lined function', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigWithFuncMultiline,
configType,
)
expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n {2}nextConfig\n\)\)/)
})
it('should parse the config with a named export as default', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
it('should parse the config with a named export as default', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigExportNamedDefault,
configType,
)
@@ -217,8 +149,8 @@ describe('parseAndInsertWithPayload', () => {
expect(modifiedConfigContent).toContain('withPayload(wrapped)')
})
it('should parse the config with a spread', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
it('should parse the config with a spread', () => {
const { modifiedConfigContent } = parseAndModifyConfigContent(
cjsConfigs.nextConfigWithSpread,
configType,
)

View File

@@ -1,27 +1,25 @@
import type { ExportDefaultExpression, ModuleItem } from '@swc/core'
import type { Program } from 'esprima-next'
import swc from '@swc/core'
import chalk from 'chalk'
import { Syntax, parseModule } from 'esprima-next'
import fs from 'fs'
import type { NextConfigType } from '../types.js'
import { log, warning } from '../utils/log.js'
export const withPayloadStatement = {
cjs: `const { withPayload } = require("@payloadcms/next/withPayload");`,
esm: `import { withPayload } from "@payloadcms/next/withPayload";`,
ts: `import { withPayload } from "@payloadcms/next/withPayload";`,
cjs: `const { withPayload } = require('@payloadcms/next/withPayload')\n`,
esm: `import { withPayload } from '@payloadcms/next/withPayload'\n`,
}
export const wrapNextConfig = async (args: {
type NextConfigType = 'cjs' | 'esm'
export const wrapNextConfig = (args: {
nextConfigPath: string
nextConfigType: NextConfigType
}) => {
const { nextConfigPath, nextConfigType: configType } = args
const configContent = fs.readFileSync(nextConfigPath, 'utf8')
const { modifiedConfigContent: newConfig, success } = await parseAndModifyConfigContent(
const { modifiedConfigContent: newConfig, success } = parseAndModifyConfigContent(
configContent,
configType,
)
@@ -36,142 +34,113 @@ export const wrapNextConfig = async (args: {
/**
* Parses config content with AST and wraps it with withPayload function
*/
export async function parseAndModifyConfigContent(
export function parseAndModifyConfigContent(
content: string,
configType: NextConfigType,
): Promise<{ modifiedConfigContent: string; success: boolean }> {
content = withPayloadStatement[configType] + '\n' + content
): { modifiedConfigContent: string; success: boolean } {
content = withPayloadStatement[configType] + content
console.log({ configType, content })
if (configType === 'cjs' || configType === 'esm') {
try {
const ast = parseModule(content, { loc: true })
if (configType === 'cjs') {
// Find `module.exports = X`
const moduleExports = ast.body.find(
(p) =>
p.type === Syntax.ExpressionStatement &&
p.expression?.type === Syntax.AssignmentExpression &&
p.expression.left?.type === Syntax.MemberExpression &&
p.expression.left.object?.type === Syntax.Identifier &&
p.expression.left.object.name === 'module' &&
p.expression.left.property?.type === Syntax.Identifier &&
p.expression.left.property.name === 'exports',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any
if (moduleExports && moduleExports.expression.right?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
moduleExports.expression.right.loc,
)
return { modifiedConfigContent, success: true }
}
return Promise.resolve({
modifiedConfigContent: content,
success: false,
})
} else if (configType === 'esm') {
const exportDefaultDeclaration = ast.body.find(
(p) => p.type === Syntax.ExportDefaultDeclaration,
) as Directive | undefined
const exportNamedDeclaration = ast.body.find(
(p) => p.type === Syntax.ExportNamedDeclaration,
) as ExportNamedDeclaration | undefined
if (!exportDefaultDeclaration && !exportNamedDeclaration) {
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
}
if (exportDefaultDeclaration && exportDefaultDeclaration.declaration?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
exportDefaultDeclaration.declaration.loc,
)
return { modifiedConfigContent, success: true }
} else if (exportNamedDeclaration) {
const exportSpecifier = exportNamedDeclaration.specifiers.find(
(s) =>
s.type === 'ExportSpecifier' &&
s.exported?.name === 'default' &&
s.local?.type === 'Identifier' &&
s.local?.name,
)
if (exportSpecifier) {
warning('Could not automatically wrap next.config.js with withPayload.')
warning('Automatic wrapping of named exports as default not supported yet.')
warnUserWrapNotSuccessful(configType)
return {
modifiedConfigContent: content,
success: false,
}
}
}
warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType)
return Promise.resolve({
modifiedConfigContent: content,
success: false,
})
}
} catch (error: unknown) {
if (error instanceof Error) {
warning(`Unable to parse Next config. Error: ${error.message} `)
warnUserWrapNotSuccessful(configType)
}
return {
modifiedConfigContent: content,
success: false,
}
let ast: Program | undefined
try {
ast = parseModule(content, { loc: true })
} catch (error: unknown) {
if (error instanceof Error) {
warning(`Unable to parse Next config. Error: ${error.message} `)
warnUserWrapNotSuccessful(configType)
}
} else if (configType === 'ts') {
const { moduleItems, parseOffset } = await compileTypeScriptFileToAST(content)
return {
modifiedConfigContent: content,
success: false,
}
}
const exportDefaultDeclaration = moduleItems.find(
(m) =>
m.type === 'ExportDefaultExpression' &&
(m.expression.type === 'Identifier' || m.expression.type === 'CallExpression'),
) as ExportDefaultExpression | undefined
if (configType === 'esm') {
const exportDefaultDeclaration = ast.body.find(
(p) => p.type === Syntax.ExportDefaultDeclaration,
) as Directive | undefined
if (exportDefaultDeclaration) {
if (!('span' in exportDefaultDeclaration.expression)) {
warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType)
return Promise.resolve({
modifiedConfigContent: content,
success: false,
})
}
const exportNamedDeclaration = ast.body.find(
(p) => p.type === Syntax.ExportNamedDeclaration,
) as ExportNamedDeclaration | undefined
const modifiedConfigContent = insertBeforeAndAfterSWC(
if (!exportDefaultDeclaration && !exportNamedDeclaration) {
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
}
if (exportDefaultDeclaration && exportDefaultDeclaration.declaration?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
exportDefaultDeclaration.expression.span,
parseOffset,
exportDefaultDeclaration.declaration.loc,
)
return { modifiedConfigContent, success: true }
} else if (exportNamedDeclaration) {
const exportSpecifier = exportNamedDeclaration.specifiers.find(
(s) =>
s.type === 'ExportSpecifier' &&
s.exported?.name === 'default' &&
s.local?.type === 'Identifier' &&
s.local?.name,
)
if (exportSpecifier) {
warning('Could not automatically wrap next.config.js with withPayload.')
warning('Automatic wrapping of named exports as default not supported yet.')
warnUserWrapNotSuccessful(configType)
return {
modifiedConfigContent: content,
success: false,
}
}
}
warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType)
return {
modifiedConfigContent: content,
success: false,
}
} else if (configType === 'cjs') {
// Find `module.exports = X`
const moduleExports = ast.body.find(
(p) =>
p.type === Syntax.ExpressionStatement &&
p.expression?.type === Syntax.AssignmentExpression &&
p.expression.left?.type === Syntax.MemberExpression &&
p.expression.left.object?.type === Syntax.Identifier &&
p.expression.left.object.name === 'module' &&
p.expression.left.property?.type === Syntax.Identifier &&
p.expression.left.property.name === 'exports',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any
if (moduleExports && moduleExports.expression.right?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
moduleExports.expression.right.loc,
)
return { modifiedConfigContent, success: true }
}
return {
modifiedConfigContent: content,
success: false,
}
}
warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType)
return Promise.resolve({
return {
modifiedConfigContent: content,
success: false,
})
}
}
function warnUserWrapNotSuccessful(configType: NextConfigType) {
// Output directions for user to update next.config.js
const withPayloadMessage = `
${chalk.bold(`Please manually wrap your existing Next config with the withPayload function. Here is an example:`)}
${chalk.bold(`Please manually wrap your existing next.config.js with the withPayload function. Here is an example:`)}
${withPayloadStatement[configType]}
@@ -179,7 +148,7 @@ function warnUserWrapNotSuccessful(configType: NextConfigType) {
// Your Next.js config here
}
${configType === 'cjs' ? 'module.exports = withPayload(nextConfig)' : 'export default withPayload(nextConfig)'}
${configType === 'esm' ? 'export default withPayload(nextConfig)' : 'module.exports = withPayload(nextConfig)'}
`
@@ -217,7 +186,7 @@ type Loc = {
start: { column: number; line: number }
}
function insertBeforeAndAfter(content: string, loc: Loc): string {
function insertBeforeAndAfter(content: string, loc: Loc) {
const { end, start } = loc
const lines = content.split('\n')
@@ -236,57 +205,3 @@ function insertBeforeAndAfter(content: string, loc: Loc): string {
return lines.join('\n')
}
function insertBeforeAndAfterSWC(
content: string,
span: ModuleItem['span'],
/**
* WARNING: This is ONLY for unit tests. Defaults to 0 otherwise.
*
* @see compileTypeScriptFileToAST
*/
parseOffset: number,
): string {
const { end: preOffsetEnd, start: preOffsetStart } = span
const start = preOffsetStart - parseOffset
const end = preOffsetEnd - parseOffset
const insert = (pos: number, text: string): string => {
return content.slice(0, pos) + text + content.slice(pos)
}
// insert ) after end
content = insert(end - 1, ')')
// insert withPayload before start
content = insert(start - 1, 'withPayload(')
return content
}
/**
* Compile typescript to AST using the swc compiler
*/
async function compileTypeScriptFileToAST(
fileContent: string,
): Promise<{ moduleItems: ModuleItem[]; parseOffset: number }> {
let parseOffset = 0
/**
* WARNING: This is ONLY for unit tests.
*
* Multiple instances of swc DO NOT reset the .span.end value.
* During unit tests, the .spawn.end value is read and accounted for.
*
* https://github.com/swc-project/swc/issues/1366
*/
if (process.env.NODE_ENV === 'test') {
parseOffset = (await swc.parse('')).span.end
}
const module = await swc.parse(fileContent, {
syntax: 'typescript',
})
return { moduleItems: module.body, parseOffset }
}

View File

@@ -85,22 +85,7 @@ export class Main {
// Detect if inside Next.js project
const nextAppDetails = await getNextAppDetails(process.cwd())
const {
hasTopLevelLayout,
isPayloadInstalled,
isSupportedNextVersion,
nextAppDir,
nextConfigPath,
nextVersion,
} = nextAppDetails
if (nextConfigPath && !isSupportedNextVersion) {
p.log.warn(
`Next.js v${nextVersion} is unsupported. Next.js >= 15 is required to use Payload.`,
)
p.outro(feedbackOutro())
process.exit(0)
}
const { hasTopLevelLayout, isPayloadInstalled, nextAppDir, nextConfigPath } = nextAppDetails
// Upgrade Payload in existing project
if (isPayloadInstalled && nextConfigPath) {

View File

@@ -57,7 +57,7 @@ interface Template {
export type PackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
export type DbType = 'mongodb' | 'postgres' | 'sqlite'
export type DbType = 'mongodb' | 'postgres'
export type DbDetails = {
dbUri: string
@@ -70,13 +70,11 @@ export type NextAppDetails = {
hasTopLevelLayout: boolean
isPayloadInstalled?: boolean
isSrcDir: boolean
isSupportedNextVersion: boolean
nextAppDir?: string
nextConfigPath?: string
nextConfigType?: NextConfigType
nextVersion: null | string
}
export type NextConfigType = 'cjs' | 'esm' | 'ts'
export type NextConfigType = 'cjs' | 'esm'
export type StorageAdapterType = 'localDisk' | 'payloadCloud' | 'vercelBlobStorage'

View File

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

View File

@@ -64,7 +64,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
forceCountFn: hasNearConstraint,
lean: true,
leanWithId: true,
limit,
offset: skip,
options,
page,
pagination,

View File

@@ -52,19 +52,9 @@ type FieldSchemaGenerator = (
buildSchemaOptions: BuildSchemaOptions,
) => void
/**
* get a field's defaultValue only if defined and not dynamic so that it can be set on the field schema
* @param field
*/
const formatDefaultValue = (field: FieldAffectingData) =>
typeof field.defaultValue !== 'undefined' && typeof field.defaultValue !== 'function'
? field.defaultValue
: undefined
const formatBaseSchema = (field: FieldAffectingData, buildSchemaOptions: BuildSchemaOptions) => {
const { disableUnique, draftsEnabled, indexSortableFields } = buildSchemaOptions
const schema: SchemaTypeOptions<unknown> = {
default: formatDefaultValue(field),
index: field.index || (!disableUnique && field.unique) || indexSortableFields || false,
required: false,
unique: (!disableUnique && field.unique) || false,
@@ -169,6 +159,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
},
}),
],
default: undefined,
}
schema.add({
@@ -183,6 +174,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
): void => {
const fieldSchema = {
type: [new mongoose.Schema({}, { _id: false, discriminatorKey: 'blockType' })],
default: undefined,
}
schema.add({
@@ -347,7 +339,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
},
coordinates: {
type: [Number],
default: formatDefaultValue(field),
default: field.defaultValue || undefined,
required: false,
},
}
@@ -428,9 +420,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
return {
...locales,
[locale]: field.hasMany
? { type: [localeSchema], default: formatDefaultValue(field) }
: localeSchema,
[locale]: field.hasMany ? { type: [localeSchema], default: undefined } : localeSchema,
}
}, {}),
localized: true,
@@ -450,7 +440,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
if (field.hasMany) {
schemaToReturn = {
type: [schemaToReturn],
default: formatDefaultValue(field),
default: undefined,
}
}
} else {
@@ -463,7 +453,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
if (field.hasMany) {
schemaToReturn = {
type: [schemaToReturn],
default: formatDefaultValue(field),
default: undefined,
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.68",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -47,8 +47,8 @@
"dependencies": {
"@payloadcms/drizzle": "workspace:*",
"console-table-printer": "2.11.2",
"drizzle-kit": "0.23.1-7816536",
"drizzle-orm": "0.32.1",
"drizzle-kit": "0.20.14-1f2c838",
"drizzle-orm": "0.29.4",
"pg": "8.11.3",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",

View File

@@ -53,7 +53,6 @@ export const connect: Connect = async function connect(
const { hotReload } = options
this.schema = {
pgSchema: this.pgSchema,
...this.tables,
...this.relations,
...this.enums,

View File

@@ -49,12 +49,6 @@ export const createMigration: CreateMigration = async function createMigration(
let drizzleJsonBefore = defaultDrizzleSnapshot
if (this.schemaName) {
drizzleJsonBefore.schemas = {
[this.schemaName]: this.schemaName,
}
}
if (!upSQL) {
// Get latest migration snapshot
const latestSnapshot = fs
@@ -71,7 +65,7 @@ export const createMigration: CreateMigration = async function createMigration(
const sqlStatementsUp = await generateMigration(drizzleJsonBefore, drizzleJsonAfter)
const sqlStatementsDown = await generateMigration(drizzleJsonAfter, drizzleJsonBefore)
const sqlExecute = 'await payload.db.drizzle.execute(sql`'
const sqlExecute = 'await db.execute(sql`'
if (sqlStatementsUp?.length) {
upSQL = `${sqlExecute}\n ${sqlStatementsUp?.join('\n')}\`)`

View File

@@ -7,11 +7,10 @@ export const defaultDrizzleSnapshot: DrizzleSnapshotJSON = {
schemas: {},
tables: {},
},
dialect: 'postgresql',
dialect: 'pg',
enums: {},
prevId: '00000000-0000-0000-0000-00000000000',
schemas: {},
sequences: {},
tables: {},
version: '7',
version: '5',
}

View File

@@ -6,11 +6,11 @@ export const getMigrationTemplate = ({
upSQL,
}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
${imports ? `${imports}\n` : ''}
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
${upSQL}
}
export async function down({ payload, req }: MigrateDownArgs): Promise<void> {
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
${downSQL}
}
`

View File

@@ -32,7 +32,6 @@ import {
updateOne,
updateVersion,
} from '@payloadcms/drizzle'
import { type PgSchema, pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { createDatabaseAdapter } from 'payload'
import type { Args, PostgresAdapter } from './types.js'
@@ -63,19 +62,12 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
const migrationDir = findMigrationDir(args.migrationDir)
let resolveInitializing
let rejectInitializing
let adapterSchema: PostgresAdapter['pgSchema']
const initializing = new Promise<void>((res, rej) => {
resolveInitializing = res
rejectInitializing = rej
})
if (args.schemaName) {
adapterSchema = pgSchema(args.schemaName)
} else {
adapterSchema = { enum: pgEnum, table: pgTable }
}
return createDatabaseAdapter<PostgresAdapter>({
name: 'postgres',
defaultDrizzleSnapshot,
@@ -91,7 +83,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
localesSuffix: args.localesSuffix || '_locales',
logger: args.logger,
operators: operatorMap,
pgSchema: adapterSchema,
pgSchema: undefined,
pool: undefined,
poolOptions: args.pool,
push: args.push,

View File

@@ -1,16 +1,24 @@
import type { Init, SanitizedCollectionConfig } from 'payload'
/* eslint-disable no-param-reassign */
import type { SanitizedCollectionConfig } from 'payload'
import type { Init } from 'payload'
import { createTableName } from '@payloadcms/drizzle'
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { createTableName } from '../../drizzle/src/createTableName.js'
import { buildTable } from './schema/build.js'
export const init: Init = function init(this: PostgresAdapter) {
if (this.schemaName) {
this.pgSchema = pgSchema(this.schemaName)
} else {
this.pgSchema = { table: pgTable }
}
if (this.payload.config.localization) {
this.enums.enum__locales = this.pgSchema.enum(
this.enums.enum__locales = pgEnum(
'_locales',
this.payload.config.localization.locales.map(({ code }) => code) as [string, ...string[]],
)

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
import type {
ForeignKeyBuilder,

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-param-reassign */
import { index, uniqueIndex } from 'drizzle-orm/pg-core'
import type { GenericColumn } from '../types.js'

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
import type { IndexBuilder, PgColumnBuilder } from 'drizzle-orm/pg-core'
import type { Field, TabAsField } from 'payload'
@@ -18,6 +19,7 @@ import {
integer,
jsonb,
numeric,
pgEnum,
text,
timestamp,
varchar,
@@ -33,7 +35,6 @@ import { buildTable } from './build.js'
import { createIndex } from './createIndex.js'
import { idToUUID } from './idToUUID.js'
import { parentIDColumnMap } from './parentIDColumnMap.js'
import { withDefault } from './withDefault.js'
type Args = {
adapter: PostgresAdapter
@@ -169,14 +170,14 @@ export const traverseFields = ({
)
}
} else {
targetTable[fieldName] = withDefault(varchar(columnName), field)
targetTable[fieldName] = varchar(columnName)
}
break
}
case 'email':
case 'code':
case 'textarea': {
targetTable[fieldName] = withDefault(varchar(columnName), field)
targetTable[fieldName] = varchar(columnName)
break
}
@@ -198,26 +199,23 @@ export const traverseFields = ({
)
}
} else {
targetTable[fieldName] = withDefault(numeric(columnName), field)
targetTable[fieldName] = numeric(columnName)
}
break
}
case 'richText':
case 'json': {
targetTable[fieldName] = withDefault(jsonb(columnName), field)
targetTable[fieldName] = jsonb(columnName)
break
}
case 'date': {
targetTable[fieldName] = withDefault(
timestamp(columnName, {
mode: 'string',
precision: 3,
withTimezone: true,
}),
field,
)
targetTable[fieldName] = timestamp(columnName, {
mode: 'string',
precision: 3,
withTimezone: true,
})
break
}
@@ -236,7 +234,7 @@ export const traverseFields = ({
throwValidationError,
})
adapter.enums[enumName] = adapter.pgSchema.enum(
adapter.enums[enumName] = pgEnum(
enumName,
field.options.map((option) => {
if (optionIsObject(option)) {
@@ -313,13 +311,13 @@ export const traverseFields = ({
}),
)
} else {
targetTable[fieldName] = withDefault(adapter.enums[enumName](fieldName), field)
targetTable[fieldName] = adapter.enums[enumName](fieldName)
}
break
}
case 'checkbox': {
targetTable[fieldName] = withDefault(boolean(columnName), field)
targetTable[fieldName] = boolean(columnName)
break
}

View File

@@ -1,17 +0,0 @@
import type { PgColumnBuilder } from 'drizzle-orm/pg-core'
import type { FieldAffectingData } from 'payload'
export const withDefault = (
column: PgColumnBuilder,
field: FieldAffectingData,
): PgColumnBuilder => {
if (typeof field.defaultValue === 'undefined' || typeof field.defaultValue === 'function')
return column
if (typeof field.defaultValue === 'string' && field.defaultValue.includes("'")) {
const escapedString = field.defaultValue.replaceAll("'", "''")
return column.default(escapedString)
}
return column.default(field.defaultValue)
}

View File

@@ -21,7 +21,6 @@ import type {
PgSchema,
PgTableWithColumns,
PgTransactionConfig,
pgEnum,
} from 'drizzle-orm/pg-core'
import type { PgTableFn } from 'drizzle-orm/pg-core/table'
import type { Payload, PayloadRequest } from 'payload'
@@ -107,13 +106,6 @@ type PostgresDrizzleAdapter = Omit<
| 'relations'
>
type Schema =
| {
enum: typeof pgEnum
table: PgTableFn
}
| PgSchema
export type PostgresAdapter = {
countDistinct: CountDistinct
defaultDrizzleSnapshot: DrizzleSnapshotJSON
@@ -133,7 +125,7 @@ export type PostgresAdapter = {
localesSuffix?: string
logger: DrizzleConfig['logger']
operators: Operators
pgSchema?: Schema
pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool
poolOptions: Args['pool']
push: boolean
@@ -141,6 +133,7 @@ export type PostgresAdapter = {
relations: Record<string, GenericRelation>
relationshipsSuffix?: string
resolveInitializing: () => void
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
schemaName?: Args['schemaName']
sessions: {
[id: string]: {
@@ -163,8 +156,6 @@ declare module 'payload' {
export interface DatabaseAdapter
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
DrizzleAdapter {
beginTransaction: (options?: PgTransactionConfig) => Promise<null | number | string>
drizzle: PostgresDB
enums: Record<string, GenericEnum>
/**
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
@@ -182,7 +173,7 @@ declare module 'payload' {
rejectInitializing: () => void
relationshipsSuffix?: string
resolveInitializing: () => void
schema: Record<string, unknown>
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
schemaName?: Args['schemaName']
tableNameMap: Map<string, string>
versionsSuffix?: string

View File

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

View File

@@ -66,7 +66,7 @@ export const createMigration: CreateMigration = async function createMigration(
const sqlStatementsUp = await generateSQLiteMigration(drizzleJsonBefore, drizzleJsonAfter)
const sqlStatementsDown = await generateSQLiteMigration(drizzleJsonAfter, drizzleJsonBefore)
// need to create tables as separate statements
const sqlExecute = 'await payload.db.drizzle.run(sql`'
const sqlExecute = 'await db.run(sql`'
if (sqlStatementsUp?.length) {
upSQL = sqlStatementsUp

View File

@@ -10,5 +10,5 @@ export const defaultDrizzleSnapshot: DrizzleSQLiteSnapshotJSON = {
enums: {},
prevId: '00000000-0000-0000-0000-00000000000',
tables: {},
version: '6',
version: '3',
}

View File

@@ -6,11 +6,11 @@ export const getMigrationTemplate = ({
upSQL,
}: MigrationTemplateArgs): string => `import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-sqlite'
${imports ? `${imports}\n` : ''}
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
${upSQL}
}
export async function down({ payload, req }: MigrateDownArgs): Promise<void> {
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
${downSQL}
}
`

View File

@@ -105,7 +105,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
versionsSuffix: args.versionsSuffix || '_v',
// DatabaseAdapter
beginTransaction: args.transactionOptions ? beginTransaction : undefined,
beginTransaction: args.transactionOptions === false ? undefined : beginTransaction,
commitTransaction,
connect,
convertPathToJSONTraversal,

View File

@@ -1,8 +1,9 @@
import type { Relation } from 'drizzle-orm'
/* eslint-disable no-param-reassign */
import type { ColumnDataType, Relation } from 'drizzle-orm'
import type {
AnySQLiteColumn,
ForeignKeyBuilder,
IndexBuilder,
SQLiteColumn,
SQLiteColumnBuilder,
SQLiteTableWithColumns,
UniqueConstraintBuilder,
@@ -31,7 +32,18 @@ import { traverseFields } from './traverseFields.js'
export type BaseExtraConfig = Record<
string,
(cols: {
[x: string]: AnySQLiteColumn
[x: string]: SQLiteColumn<{
baseColumn: never
columnType: string
data: unknown
dataType: ColumnDataType
driverParam: unknown
enumValues: string[]
hasDefault: false
name: string
notNull: false
tableName: string
}>
}) => ForeignKeyBuilder | IndexBuilder | UniqueConstraintBuilder
>

View File

@@ -1,7 +1,8 @@
import type { AnySQLiteColumn} from 'drizzle-orm/sqlite-core';
/* eslint-disable no-param-reassign */
import { index, uniqueIndex } from 'drizzle-orm/sqlite-core'
import type { GenericColumn } from '../types.js'
type CreateIndexArgs = {
columnName: string
name: string | string[]
@@ -10,7 +11,7 @@ type CreateIndexArgs = {
}
export const createIndex = ({ name, columnName, tableName, unique }: CreateIndexArgs) => {
return (table: { [x: string]: AnySQLiteColumn }) => {
return (table: { [x: string]: GenericColumn }) => {
let columns
if (Array.isArray(name)) {
columns = name

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
import type { IndexBuilder, SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core'
import type { Field, TabAsField } from 'payload'
@@ -29,7 +30,6 @@ import { buildTable } from './build.js'
import { createIndex } from './createIndex.js'
import { getIDColumn } from './getIDColumn.js'
import { idToUUID } from './idToUUID.js'
import { withDefault } from './withDefault.js'
type Args = {
adapter: SQLiteAdapter
@@ -166,14 +166,14 @@ export const traverseFields = ({
)
}
} else {
targetTable[fieldName] = withDefault(text(columnName), field)
targetTable[fieldName] = text(columnName)
}
break
}
case 'email':
case 'code':
case 'textarea': {
targetTable[fieldName] = withDefault(text(columnName), field)
targetTable[fieldName] = text(columnName)
break
}
@@ -195,19 +195,19 @@ export const traverseFields = ({
)
}
} else {
targetTable[fieldName] = withDefault(numeric(columnName), field)
targetTable[fieldName] = numeric(columnName)
}
break
}
case 'richText':
case 'json': {
targetTable[fieldName] = withDefault(text(columnName, { mode: 'json' }), field)
targetTable[fieldName] = text(columnName, { mode: 'json' })
break
}
case 'date': {
targetTable[fieldName] = withDefault(text(columnName), field)
targetTable[fieldName] = text(columnName)
break
}
@@ -295,13 +295,13 @@ export const traverseFields = ({
}),
)
} else {
targetTable[fieldName] = withDefault(text(fieldName, { enum: options }), field)
targetTable[fieldName] = text(fieldName, { enum: options })
}
break
}
case 'checkbox': {
targetTable[fieldName] = withDefault(integer(columnName, { mode: 'boolean' }), field)
targetTable[fieldName] = integer(columnName, { mode: 'boolean' })
break
}

View File

@@ -1,17 +0,0 @@
import type { SQLiteColumnBuilder } from 'drizzle-orm/sqlite-core'
import type { FieldAffectingData } from 'payload'
export const withDefault = (
column: SQLiteColumnBuilder,
field: FieldAffectingData,
): SQLiteColumnBuilder => {
if (typeof field.defaultValue === 'undefined' || typeof field.defaultValue === 'function')
return column
if (typeof field.defaultValue === 'string' && field.defaultValue.includes("'")) {
const escapedString = field.defaultValue.replaceAll("'", "''")
return column.default(escapedString)
}
return column.default(field.defaultValue)
}

View File

@@ -1,10 +1,10 @@
import type { Client, Config, ResultSet } from '@libsql/client'
import type { Operators } from '@payloadcms/drizzle'
import type { BuildQueryJoinAliases, DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { DrizzleConfig, Relation, Relations, SQL } from 'drizzle-orm'
import type { ColumnDataType, DrizzleConfig, Relation, Relations, SQL } from 'drizzle-orm'
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type {
AnySQLiteColumn,
SQLiteColumn,
SQLiteInsertOnConflictDoUpdateConfig,
SQLiteTableWithColumns,
SQLiteTransactionConfig,
@@ -25,8 +25,24 @@ export type Args = {
versionsSuffix?: string
}
export type GenericColumn = SQLiteColumn<
{
baseColumn: never
columnType: string
data: unknown
dataType: ColumnDataType
driverParam: unknown
enumValues: string[]
hasDefault: false
name: string
notNull: false
tableName: string
},
object
>
export type GenericColumns = {
[x: string]: AnySQLiteColumn
[x: string]: GenericColumn
}
export type GenericTable = SQLiteTableWithColumns<{
@@ -116,10 +132,12 @@ export type SQLiteAdapter = {
export type IDType = 'integer' | 'numeric' | 'text'
export type MigrateUpArgs = {
db: LibSQLDatabase
payload: Payload
req?: Partial<PayloadRequest>
}
export type MigrateDownArgs = {
db: LibSQLDatabase
payload: Payload
req?: Partial<PayloadRequest>
}
@@ -128,8 +146,6 @@ declare module 'payload' {
export interface DatabaseAdapter
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
DrizzleAdapter {
beginTransaction: (options?: SQLiteTransactionConfig) => Promise<null | number | string>
drizzle: LibSQLDatabase
/**
* An object keyed on each table, with a key value pair where the constraint name is the key, followed by the dot-notation field name
* Used for returning properly formed errors from unique fields

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/drizzle",
"version": "3.0.0-beta.73",
"version": "3.0.0-beta.36",
"description": "A library of shared functions used by different payload database adapters",
"homepage": "https://payloadcms.com",
"repository": {
@@ -39,7 +39,7 @@
},
"dependencies": {
"console-table-printer": "2.11.2",
"drizzle-orm": "0.32.1",
"drizzle-orm": "0.29.4",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",
"uuid": "9.0.0"

View File

@@ -1,4 +1,5 @@
import type { Count , SanitizedCollectionConfig } from 'payload'
import type { Count } from 'payload'
import type { SanitizedCollectionConfig } from 'payload'
import toSnakeCase from 'to-snake-case'
@@ -14,7 +15,7 @@ export const count: Count = async function count(
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const { joins, where } = await buildQuery({
adapter: this,

View File

@@ -10,7 +10,7 @@ export const create: Create = async function create(
this: DrizzleAdapter,
{ collection: collectionSlug, data, req },
) {
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))

View File

@@ -10,7 +10,7 @@ export async function createGlobal<T extends Record<string, unknown>>(
this: DrizzleAdapter,
{ slug, data, req = {} as PayloadRequest }: CreateGlobalArgs,
): Promise<T> {
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))

View File

@@ -12,7 +12,7 @@ export async function createGlobalVersion<T extends TypeWithID>(
this: DrizzleAdapter,
{ autosave, globalSlug, req = {} as PayloadRequest, versionData }: CreateGlobalVersionArgs,
) {
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug)
const tableName = this.tableNameMap.get(`_${toSnakeCase(global.slug)}${this.versionsSuffix}`)

View File

@@ -18,7 +18,7 @@ export async function createVersion<T extends TypeWithID>(
versionData,
}: CreateVersionArgs<T>,
) {
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const defaultTableName = toSnakeCase(collection.slug)

View File

@@ -11,7 +11,7 @@ export const deleteMany: DeleteMany = async function deleteMany(
this: DrizzleAdapter,
{ collection, req = {} as PayloadRequest, where },
) {
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const collectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug))

View File

@@ -14,7 +14,7 @@ export const deleteOne: DeleteOne = async function deleteOne(
this: DrizzleAdapter,
{ collection: collectionSlug, req = {} as PayloadRequest, where: whereArg },
) {
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))

View File

@@ -12,7 +12,7 @@ export const deleteVersions: DeleteVersions = async function deleteVersion(
this: DrizzleAdapter,
{ collection, locale, req = {} as PayloadRequest, where: where },
) {
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const tableName = this.tableNameMap.get(

View File

@@ -6,7 +6,6 @@ import type { DrizzleAdapter, DrizzleTransaction } from '../types.js'
export const beginTransaction: BeginTransaction = async function beginTransaction(
this: DrizzleAdapter,
options: DrizzleAdapter['transactionOptions'],
) {
let id
try {
@@ -42,7 +41,7 @@ export const beginTransaction: BeginTransaction = async function beginTransactio
}
transactionReady()
})
}, options || this.transactionOptions)
}, this.transactionOptions)
.catch(() => {
// swallow
})

View File

@@ -1,4 +1,5 @@
import type { Field, SanitizedConfig, TabAsField } from 'payload'
import type { Field, SanitizedConfig , TabAsField } from 'payload'
import { fieldAffectsData } from 'payload/shared'

View File

@@ -120,7 +120,6 @@ export type RequireDrizzleKit = () => {
pushSchema: (
schema: Record<string, unknown>,
drizzle: DrizzleAdapter['drizzle'],
filterSchema?: string[],
) => Promise<{ apply; hasDataLoss; warnings }>
}

View File

@@ -12,7 +12,7 @@ export const updateOne: UpdateOne = async function updateOne(
this: DrizzleAdapter,
{ id, collection: collectionSlug, data, draft, locale, req, where: whereArg },
) {
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
const whereToUse = whereArg || { id: { equals: id } }

View File

@@ -10,7 +10,7 @@ export async function updateGlobal<T extends Record<string, unknown>>(
this: DrizzleAdapter,
{ slug, data, req = {} as PayloadRequest }: UpdateGlobalArgs,
): Promise<T> {
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const globalConfig = this.payload.globals.config.find((config) => config.slug === slug)
const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug))

View File

@@ -25,7 +25,7 @@ export async function updateGlobalVersion<T extends TypeWithID>(
where: whereArg,
}: UpdateGlobalVersionArgs<T>,
) {
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const globalConfig: SanitizedGlobalConfig = this.payload.globals.config.find(
({ slug }) => slug === global,
)

View File

@@ -25,7 +25,7 @@ export async function updateVersion<T extends TypeWithID>(
where: whereArg,
}: UpdateVersionArgs<T>,
) {
const db = this.sessions[await req?.transactionID]?.db || this.drizzle
const db = this.sessions[await req.transactionID]?.db || this.drizzle
const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config
const whereToUse = whereArg || { id: { equals: id } }
const tableName = this.tableNameMap.get(

View File

@@ -4,10 +4,8 @@ import { fieldAffectsData, fieldHasSubFields } from 'payload/shared'
export const hasLocalesTable = (fields: Field[]): boolean => {
return fields.some((field) => {
// arrays always get a separate table
if (field.type === 'array') return false
if (fieldAffectsData(field) && field.localized) return true
if (fieldHasSubFields(field)) return hasLocalesTable(field.fields)
if (fieldHasSubFields(field) && field.type !== 'array') return hasLocalesTable(field.fields)
if (field.type === 'tabs') return field.tabs.some((tab) => hasLocalesTable(tab.fields))
return false
})

View File

@@ -5,7 +5,7 @@ export const migrationTableExists = async (adapter: DrizzleAdapter): Promise<boo
if (adapter.name === 'postgres') {
const prependSchema = adapter.schemaName ? `"${adapter.schemaName}".` : ''
statement = `SELECT to_regclass('${prependSchema}"payload_migrations"') AS exists;`
statement = `SELECT to_regclass('${prependSchema}"payload_migrations"') exists;`
}
if (adapter.name === 'sqlite') {

View File

@@ -12,11 +12,7 @@ export const pushDevSchema = async (adapter: DrizzleAdapter) => {
const { pushSchema } = adapter.requireDrizzleKit()
// This will prompt if clarifications are needed for Drizzle to push new schema
const { apply, hasDataLoss, warnings } = await pushSchema(
adapter.schema,
adapter.drizzle,
adapter.schemaName ? [adapter.schemaName] : undefined,
)
const { apply, hasDataLoss, warnings } = await pushSchema(adapter.schema, adapter.drizzle)
if (warnings.length) {
let message = `Warnings detected during schema push: \n\n${warnings.join('\n')}\n\n`

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ export const index = deepMerge(
},
},
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/rules-of-hooks': 'warn',
},
},
)

View File

@@ -33,7 +33,7 @@
"eslint-plugin-react-hooks": "5.1.0-rc-85acf2d195-20240711",
"eslint-plugin-regexp": "2.6.0",
"globals": "15.8.0",
"typescript": "5.5.4",
"typescript": "5.5.3",
"typescript-eslint": "7.16.0"
}
}

View File

@@ -32,7 +32,7 @@
"eslint-plugin-react-hooks": "5.1.0-rc-85acf2d195-20240711",
"eslint-plugin-regexp": "2.6.0",
"globals": "15.8.0",
"typescript": "5.5.4",
"typescript": "5.5.3",
"typescript-eslint": "7.16.0"
}
}

View File

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

View File

@@ -1,3 +1,3 @@
export { GraphQLJSON, GraphQLJSONObject } from '../packages/graphql-type-json/index.js'
export { buildPaginatedListType } from '../schema/buildPaginatedListType.js'
export * as GraphQL from 'graphql'
export { default as GraphQL } from 'graphql'

View File

@@ -7,7 +7,6 @@ import type { Context } from '../types.js'
export type Resolver = (
_: unknown,
args: {
draft?: boolean
id: number | string
},
context: {
@@ -21,7 +20,6 @@ export default function restoreVersionResolver(collection: Collection): Resolver
id: args.id,
collection,
depth: 0,
draft: args.draft,
req: isolateObjectProperty(context.req, 'transactionID'),
}

View File

@@ -7,7 +7,6 @@ import type { Context } from '../types.js'
type Resolver = (
_: unknown,
args: {
draft?: boolean
id: number | string
},
context: {
@@ -19,7 +18,6 @@ export default function restoreVersionResolver(globalConfig: SanitizedGlobalConf
const options = {
id: args.id,
depth: 0,
draft: args.draft,
globalConfig,
req: isolateObjectProperty(context.req, 'transactionID'),
}

View File

@@ -342,7 +342,6 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
type: collection.graphQL.type,
args: {
id: { type: versionIDType },
draft: { type: GraphQLBoolean },
},
resolve: restoreVersionResolver(collection),
}

View File

@@ -133,7 +133,6 @@ function initGlobalsGraphQL({ config, graphqlResult }: InitGlobalsGraphQLArgs):
type: graphqlResult.globals.graphQL[slug].versionType,
args: {
id: { type: idType },
draft: { type: GraphQLBoolean },
...(config.localization
? {
fallbackLocale: { type: graphqlResult.types.fallbackLocaleInputType },
@@ -172,7 +171,6 @@ function initGlobalsGraphQL({ config, graphqlResult }: InitGlobalsGraphQLArgs):
type: graphqlResult.globals.graphQL[slug].type,
args: {
id: { type: idType },
draft: { type: GraphQLBoolean },
},
resolve: restoreVersionResolver(global),
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
'use client'
import { useDocumentInfo } from '@payloadcms/ui'
import React from 'react'
import React, { Fragment } from 'react'
import { baseClass } from '../../Tab/index.js'
@@ -12,14 +12,13 @@ export const VersionsPill: React.FC = () => {
// documents that are version enabled _always_ have at least one version
const hasVersions = versions?.totalDocs > 0
if (hasVersions)
return (
<span
className={[`${baseClass}__count`, hasVersions ? `${baseClass}__count--has-count` : '']
.filter(Boolean)
.join(' ')}
>
{versions.totalDocs.toString()}
</span>
)
return (
<span
className={[`${baseClass}__count`, hasVersions ? `${baseClass}__count--has-count` : '']
.filter(Boolean)
.join(' ')}
>
{hasVersions ? versions.totalDocs.toString() : <Fragment>&nbsp;</Fragment>}
</span>
)
}

View File

@@ -85,18 +85,9 @@ export const DefaultNavClient: React.FC = () => {
const LinkElement = Link || 'a'
const activeCollection = window?.location?.pathname
?.split('/')
.find(
(_, index, arr) =>
arr[index - 1] === 'collections' || arr[index - 1] === 'globals',
)
return (
<LinkElement
className={[`${baseClass}__link`, activeCollection === entity?.slug && `active`]
.filter(Boolean)
.join(' ')}
className={`${baseClass}__link`}
href={href}
id={id}
key={i}

View File

@@ -134,8 +134,12 @@ export const POST =
resHeaders.append(key, headers[key])
}
if (req.responseHeaders) {
mergeHeaders(req.responseHeaders, resHeaders)
}
return new Response(apiResponse.body, {
headers: req.responseHeaders ? mergeHeaders(req.responseHeaders, resHeaders) : resHeaders,
headers: resHeaders,
status: apiResponse.status,
})
}

View File

@@ -14,7 +14,6 @@ export const restoreVersion: CollectionRouteHandlerWithID = async ({
}) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const draft = searchParams.get('draft')
const id = sanitizeCollectionID({
id: incomingID,
@@ -26,7 +25,6 @@ export const restoreVersion: CollectionRouteHandlerWithID = async ({
id,
collection,
depth: isNumber(depth) ? Number(depth) : undefined,
draft: draft === 'true' ? true : undefined,
req,
})

View File

@@ -9,12 +9,10 @@ import { headersWithCors } from '../../../utilities/headersWithCors.js'
export const restoreVersion: GlobalRouteHandlerWithID = async ({ id, globalConfig, req }) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const draft = searchParams.get('draft')
const doc = await restoreVersionOperationGlobal({
id,
depth: isNumber(depth) ? Number(depth) : undefined,
draft: draft === 'true' ? true : undefined,
globalConfig,
req,
})

View File

@@ -167,13 +167,7 @@ const handleCustomEndpoints = async ({
if (res instanceof Response) {
if (req.responseHeaders) {
const mergedResponse = new Response(res.body, {
headers: mergeHeaders(req.responseHeaders, res.headers),
status: res.status,
statusText: res.statusText,
})
return mergedResponse
mergeHeaders(req.responseHeaders, res.headers)
}
return res
@@ -385,13 +379,7 @@ export const GET =
if (res instanceof Response) {
if (req.responseHeaders) {
const mergedResponse = new Response(res.body, {
headers: mergeHeaders(req.responseHeaders, res.headers),
status: res.status,
statusText: res.statusText,
})
return mergedResponse
mergeHeaders(req.responseHeaders, res.headers)
}
return res
@@ -567,13 +555,7 @@ export const POST =
if (res instanceof Response) {
if (req.responseHeaders) {
const mergedResponse = new Response(res.body, {
headers: mergeHeaders(req.responseHeaders, res.headers),
status: res.status,
statusText: res.statusText,
})
return mergedResponse
mergeHeaders(req.responseHeaders, res.headers)
}
return res
@@ -661,13 +643,7 @@ export const DELETE =
if (res instanceof Response) {
if (req.responseHeaders) {
const mergedResponse = new Response(res.body, {
headers: mergeHeaders(req.responseHeaders, res.headers),
status: res.status,
statusText: res.statusText,
})
return mergedResponse
mergeHeaders(req.responseHeaders, res.headers)
}
return res
@@ -756,13 +732,7 @@ export const PATCH =
if (res instanceof Response) {
if (req.responseHeaders) {
const mergedResponse = new Response(res.body, {
headers: mergeHeaders(req.responseHeaders, res.headers),
status: res.status,
statusText: res.statusText,
})
return mergedResponse
mergeHeaders(req.responseHeaders, res.headers)
}
return res

View File

@@ -7,7 +7,7 @@ import type {
import { notFound } from 'next/navigation.js'
import { getRouteWithoutAdmin, isAdminAuthRoute, isAdminRoute } from './shared.js'
import { isAdminAuthRoute, isAdminRoute } from './shared.js'
export const handleAdminPage = ({
adminRoute,
@@ -20,9 +20,9 @@ export const handleAdminPage = ({
permissions: Permissions
route: string
}) => {
if (isAdminRoute({ adminRoute, config, route })) {
const routeWithoutAdmin = getRouteWithoutAdmin({ adminRoute, route })
const routeSegments = routeWithoutAdmin.split('/').filter(Boolean)
if (isAdminRoute(route, adminRoute)) {
const baseAdminRoute = adminRoute && adminRoute !== '/' ? route.replace(adminRoute, '') : route
const routeSegments = baseAdminRoute.split('/').filter(Boolean)
const [entityType, entitySlug, createOrID] = routeSegments
const collectionSlug = entityType === 'collections' ? entitySlug : undefined
const globalSlug = entityType === 'globals' ? entitySlug : undefined
@@ -47,7 +47,7 @@ export const handleAdminPage = ({
}
}
if (!permissions.canAccessAdmin && !isAdminAuthRoute({ adminRoute, config, route })) {
if (!permissions.canAccessAdmin && !isAdminAuthRoute(config, route, adminRoute)) {
notFound()
}

View File

@@ -22,7 +22,7 @@ export const handleAuthRedirect = ({
routes: { admin: adminRoute },
} = config
if (!isAdminAuthRoute({ adminRoute, config, route })) {
if (!isAdminAuthRoute(config, route, adminRoute)) {
if (searchParams && 'redirect' in searchParams) delete searchParams.redirect
const redirectRoute = encodeURIComponent(
@@ -36,7 +36,7 @@ export const handleAuthRedirect = ({
const customLoginRoute =
typeof redirectUnauthenticatedUser === 'string' ? redirectUnauthenticatedUser : undefined
const loginRoute = isAdminRoute({ adminRoute, config, route })
const loginRoute = isAdminRoute(route, adminRoute)
? adminLoginRoute
: customLoginRoute || loginRouteFromConfig

View File

@@ -11,42 +11,16 @@ const authRouteKeys: (keyof SanitizedConfig['admin']['routes'])[] = [
'reset',
]
export const isAdminRoute = ({
adminRoute,
config,
route,
}: {
adminRoute: string
config: SanitizedConfig
route: string
}): boolean => {
return route.startsWith(adminRoute) && !isAdminAuthRoute({ adminRoute, config, route })
export const isAdminRoute = (route: string, adminRoute: string) => {
return route.startsWith(adminRoute)
}
export const isAdminAuthRoute = ({
adminRoute,
config,
route,
}: {
adminRoute: string
config: SanitizedConfig
route: string
}): boolean => {
export const isAdminAuthRoute = (config: SanitizedConfig, route: string, adminRoute: string) => {
const authRoutes = config.admin?.routes
? Object.entries(config.admin.routes)
.filter(([key]) => authRouteKeys.includes(key as keyof SanitizedConfig['admin']['routes']))
.map(([_, value]) => value)
: []
return authRoutes.some((r) => getRouteWithoutAdmin({ adminRoute, route }).startsWith(r))
}
export const getRouteWithoutAdmin = ({
adminRoute,
route,
}: {
adminRoute: string
route: string
}): string => {
return adminRoute && adminRoute !== '/' ? route.replace(adminRoute, '') : route
return authRoutes.some((r) => route.replace(adminRoute, '').startsWith(r))
}

View File

@@ -1,11 +1,33 @@
export const mergeHeaders = (sourceHeaders: Headers, destinationHeaders: Headers): Headers => {
// Create a new Headers object
const combinedHeaders = new Headers(destinationHeaders)
const headersToJoin = ['set-cookie', 'warning', 'www-authenticate', 'proxy-authenticate', 'vary']
// Append sourceHeaders to combinedHeaders
sourceHeaders.forEach((value, key) => {
combinedHeaders.append(key, value)
export function mergeHeaders(sourceHeaders: Headers, destinationHeaders: Headers): void {
// Create a map to store combined headers
const combinedHeaders = new Headers()
// Add existing destination headers to the combined map
destinationHeaders.forEach((value, key) => {
combinedHeaders.set(key, value)
})
return combinedHeaders
// Add source headers to the combined map, joining specific headers
sourceHeaders.forEach((value, key) => {
const lowerKey = key.toLowerCase()
if (headersToJoin.includes(lowerKey)) {
if (combinedHeaders.has(key)) {
combinedHeaders.set(key, `${combinedHeaders.get(key)}, ${value}`)
} else {
combinedHeaders.set(key, value)
}
} else {
combinedHeaders.set(key, value)
}
})
// Clear the destination headers and set the combined headers
destinationHeaders.forEach((_, key) => {
destinationHeaders.delete(key)
})
combinedHeaders.forEach((value, key) => {
destinationHeaders.append(key, value)
})
}

View File

@@ -35,17 +35,18 @@ export const CreateFirstUserClient: React.FC<{
const fieldMap = getFieldMap({ collectionSlug: userSlug })
const onChange: FormProps['onChange'][0] = React.useCallback(
async ({ formState: prevFormState }) =>
getFormState({
async ({ formState: prevFormState }) => {
return getFormState({
apiRoute,
body: {
collectionSlug: userSlug,
formState: prevFormState,
operation: 'create',
schemaPath: `_${userSlug}.auth`,
schemaPath: userSlug,
},
serverURL,
}),
})
},
[apiRoute, userSlug, serverURL],
)
@@ -63,15 +64,14 @@ export const CreateFirstUserClient: React.FC<{
<LoginField required={requireEmail} type="email" />
)}
<PasswordField
autoComplete="off"
label={t('authentication:newPassword')}
name="password"
path="password"
required
/>
<ConfirmPasswordField />
<RenderFields
fieldMap={fieldMap}
forceRender
operation="create"
path=""
readOnly={false}

View File

@@ -1,10 +1,10 @@
import type { AdminViewProps } from 'payload'
import type { AdminViewProps, Field } from 'payload'
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
import React from 'react'
import type { LoginFieldProps } from '../Login/LoginField/index.js'
import { getDocumentData } from '../Document/getDocumentData.js'
import { CreateFirstUserClient } from './index.client.js'
import './index.scss'
@@ -12,7 +12,6 @@ export { generateCreateFirstUserMetadata } from './meta.js'
export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageResult }) => {
const {
locale,
req,
req: {
payload: {
@@ -27,6 +26,7 @@ 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 loginWithEmail = !loginWithUsername || loginWithUsername.allowEmailLogin
const emailRequired = loginWithUsername && loginWithUsername.requireEmail
let loginType: LoginFieldProps['type'] = loginWithUsername ? 'username' : 'email'
@@ -34,11 +34,42 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
loginType = 'emailOrUsername'
}
const { formState } = await getDocumentData({
collectionConfig,
locale,
const emailField = {
name: 'email',
type: 'email',
label: req.t('general:emailAddress'),
required: emailRequired ? true : false,
}
const usernameField = {
name: 'username',
type: 'text',
label: req.t('authentication:username'),
required: true,
}
const fields = [
...(loginWithUsername ? [usernameField] : []),
...(emailRequired || loginWithEmail ? [emailField] : []),
{
name: 'password',
type: 'text',
label: req.t('general:password'),
required: true,
},
{
name: 'confirm-password',
type: 'text',
label: req.t('authentication:confirmPassword'),
required: true,
},
]
const formState = await buildStateFromSchema({
fieldSchema: fields as Field[],
operation: 'create',
preferences: { fields: {} },
req,
schemaPath: `_${collectionConfig.slug}.auth`,
})
return (

View File

@@ -15,11 +15,8 @@ export const getDocumentData = async (args: {
id?: number | string
locale: Locale
req: PayloadRequest
schemaPath?: string
}): Promise<Data> => {
const { id, collectionConfig, globalConfig, locale, req, schemaPath: schemaPathFromProps } = args
const schemaPath = schemaPathFromProps || collectionConfig?.slug || globalConfig?.slug
const { id, collectionConfig, globalConfig, locale, req } = args
try {
const formState = await buildFormState({
@@ -31,7 +28,7 @@ export const getDocumentData = async (args: {
globalSlug: globalConfig?.slug,
locale: locale?.code,
operation: (collectionConfig && id) || globalConfig ? 'update' : 'create',
schemaPath,
schemaPath: collectionConfig?.slug || globalConfig?.slug,
},
},
})

View File

@@ -78,7 +78,7 @@ export const APIKey: React.FC<{ enabled: boolean; readOnly?: boolean }> = ({
if (!apiKeyValue && enabled) {
setValue(initialAPIKey)
}
if (!enabled && apiKeyValue) {
if (!enabled) {
setValue(null)
}
}, [apiKeyValue, enabled, setValue, initialAPIKey])
@@ -100,7 +100,6 @@ export const APIKey: React.FC<{ enabled: boolean; readOnly?: boolean }> = ({
<div className={[fieldBaseClass, 'api-key', 'read-only'].filter(Boolean).join(' ')}>
<FieldLabel CustomLabel={APIKeyLabel} htmlFor={path} />
<input
aria-label="API Key"
className={highlightedField ? 'highlight' : undefined}
disabled
id="apiKey"

View File

@@ -7,15 +7,13 @@ import {
EmailField,
PasswordField,
TextField,
useAuth,
useConfig,
useDocumentInfo,
useFormFields,
useFormModified,
useTranslation,
} from '@payloadcms/ui'
import { email as emailValidation } from 'payload/shared'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { toast } from 'sonner'
import type { Props } from './types.js'
@@ -35,17 +33,13 @@ export const Auth: React.FC<Props> = (props) => {
operation,
readOnly,
requirePassword,
setSchemaPath,
setValidateBeforeSubmit,
useAPIKey,
username,
verify,
} = props
const { permissions } = useAuth()
const [changingPassword, setChangingPassword] = useState(requirePassword)
const enableAPIKey = useFormFields(([fields]) => (fields && fields?.enableAPIKey) || null)
const forceOpenChangePassword = useFormFields(([fields]) => (fields && fields?.password) || null)
const dispatchFields = useFormFields((reducer) => reducer[1])
const modified = useFormModified()
const { i18n, t } = useTranslation()
@@ -56,50 +50,16 @@ export const Auth: React.FC<Props> = (props) => {
serverURL,
} = useConfig()
const hasPermissionToUnlock: boolean = useMemo(() => {
const collection = permissions?.collections?.[collectionSlug]
if (collection) {
const unlock = 'unlock' in collection ? collection.unlock : undefined
if (unlock) {
// current types for permissions do not include auth permissions, this will be fixed in another branch soon, for now we need to ignore the types
// @todo: fix types
// @ts-expect-error
return unlock.permission
}
}
return false
}, [permissions, collectionSlug])
const handleChangePassword = useCallback(
(showPasswordFields: boolean) => {
if (showPasswordFields) {
setValidateBeforeSubmit(true)
setSchemaPath(`_${collectionSlug}.auth`)
dispatchFields({
type: 'UPDATE',
errorMessage: t('validation:required'),
path: 'password',
valid: false,
})
dispatchFields({
type: 'UPDATE',
errorMessage: t('validation:required'),
path: 'confirm-password',
valid: false,
})
} else {
setValidateBeforeSubmit(false)
setSchemaPath(collectionSlug)
(state: boolean) => {
if (!state) {
dispatchFields({ type: 'REMOVE', path: 'password' })
dispatchFields({ type: 'REMOVE', path: 'confirm-password' })
}
setChangingPassword(showPasswordFields)
setChangingPassword(state)
},
[dispatchFields, t, collectionSlug, setSchemaPath, setValidateBeforeSubmit],
[dispatchFields],
)
const unlock = useCallback(async () => {
@@ -120,7 +80,7 @@ export const Auth: React.FC<Props> = (props) => {
} else {
toast.error(t('authentication:failedToUnlock'))
}
}, [i18n, serverURL, api, collectionSlug, email, username, t, loginWithUsername])
}, [i18n, serverURL, api, collectionSlug, email, username, t])
useEffect(() => {
if (!modified) {
@@ -134,8 +94,6 @@ export const Auth: React.FC<Props> = (props) => {
const disabled = readOnly || isInitializing
const showPasswordFields = changingPassword || forceOpenChangePassword
return (
<div className={[baseClass, className].filter(Boolean).join(' ')}>
{!disableLocalStrategy && (
@@ -159,33 +117,22 @@ export const Auth: React.FC<Props> = (props) => {
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: {},
})
}
/>
)}
{(showPasswordFields || requirePassword) && (
{(changingPassword || requirePassword) && (
<div className={`${baseClass}__changing-password`}>
<PasswordField
autoComplete="off"
disabled={disabled}
label={t('authentication:newPassword')}
name="password"
path="password"
required
/>
<ConfirmPasswordField disabled={readOnly} />
</div>
)}
<div className={`${baseClass}__controls`}>
{showPasswordFields && !requirePassword && (
{changingPassword && !requirePassword && (
<Button
buttonStyle="secondary"
disabled={disabled}
@@ -195,7 +142,7 @@ export const Auth: React.FC<Props> = (props) => {
{t('general:cancel')}
</Button>
)}
{!showPasswordFields && !requirePassword && (
{!changingPassword && !requirePassword && (
<Button
buttonStyle="secondary"
disabled={disabled}
@@ -206,11 +153,11 @@ export const Auth: React.FC<Props> = (props) => {
{t('authentication:changePassword')}
</Button>
)}
{operation === 'update' && hasPermissionToUnlock && (
{operation === 'update' && (
<Button
buttonStyle="secondary"
disabled={disabled}
onClick={() => void unlock()}
onClick={() => unlock()}
size="small"
>
{t('authentication:forceUnlock')}

View File

@@ -9,8 +9,6 @@ export type Props = {
operation: 'create' | 'update'
readOnly: boolean
requirePassword?: boolean
setSchemaPath: (path: string) => void
setValidateBeforeSubmit: (validate: boolean) => void
useAPIKey?: boolean
username: string
verify?: VerifyConfig | boolean

View File

@@ -17,7 +17,7 @@ import {
} from '@payloadcms/ui'
import { formatAdminURL, getFormState } from '@payloadcms/ui/shared'
import { useRouter, useSearchParams } from 'next/navigation.js'
import React, { Fragment, useCallback, useState } from 'react'
import React, { Fragment, useCallback } from 'react'
import { LeaveWithoutSaving } from '../../../elements/LeaveWithoutSaving/index.js'
import { Auth } from './Auth/index.js'
@@ -102,9 +102,6 @@ export const DefaultEditView: React.FC = () => {
const classes = [baseClass, id && `${baseClass}--is-editing`].filter(Boolean).join(' ')
const [schemaPath, setSchemaPath] = React.useState(entitySlug)
const [validateBeforeSubmit, setValidateBeforeSubmit] = useState(false)
const onSave = useCallback(
(json) => {
reportUpdate({
@@ -161,6 +158,7 @@ export const DefaultEditView: React.FC = () => {
const onChange: FormProps['onChange'][0] = useCallback(
async ({ formState: prevFormState }) => {
const docPreferences = await getDocPreferences()
return getFormState({
apiRoute,
body: {
@@ -170,12 +168,12 @@ export const DefaultEditView: React.FC = () => {
formState: prevFormState,
globalSlug,
operation,
schemaPath,
schemaPath: entitySlug,
},
serverURL,
})
},
[apiRoute, collectionSlug, schemaPath, getDocPreferences, globalSlug, id, operation, serverURL],
[serverURL, apiRoute, id, operation, entitySlug, collectionSlug, globalSlug, getDocPreferences],
)
return (
@@ -184,7 +182,7 @@ export const DefaultEditView: React.FC = () => {
<Form
action={action}
className={`${baseClass}__form`}
disableValidationOnSubmit={!validateBeforeSubmit}
disableValidationOnSubmit
disabled={isInitializing || !hasSavePermission}
initialState={!isInitializing && initialState}
isInitializing={isInitializing}
@@ -233,8 +231,6 @@ export const DefaultEditView: React.FC = () => {
operation={operation}
readOnly={!hasSavePermission}
requirePassword={!id}
setSchemaPath={setSchemaPath}
setValidateBeforeSubmit={setValidateBeforeSubmit}
useAPIKey={auth.useAPIKey}
username={data?.username}
verify={auth.verify}
@@ -259,7 +255,7 @@ export const DefaultEditView: React.FC = () => {
docPermissions={docPermissions}
fieldMap={fieldMap}
readOnly={!hasSavePermission}
schemaPath={schemaPath}
schemaPath={entitySlug}
/>
{AfterDocument}
</Form>

View File

@@ -1,11 +1,11 @@
'use client'
import type { FormProps } from '@payloadcms/ui'
import type { FieldMap } from '@payloadcms/ui/utilities/buildComponentMap'
import type {
ClientCollectionConfig,
ClientConfig,
ClientGlobalConfig,
Data,
FieldMap,
LivePreviewConfig,
} from 'payload'

View File

@@ -18,7 +18,6 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true })
autoComplete="email"
label={t('general:email')}
name="email"
path="email"
required={required}
validate={(value) =>
email(value, {
@@ -40,7 +39,6 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true })
<TextField
label={t('authentication:username')}
name="username"
path="username"
required
validate={(value) =>
username(value, {
@@ -67,7 +65,6 @@ export const LoginField: React.FC<LoginFieldProps> = ({ type, required = true })
<TextField
label={t('authentication:emailOrUsername')}
name="username"
path="username"
required
validate={(value) => {
const passesUsername = username(value, {

View File

@@ -6,10 +6,11 @@ import React from 'react'
const baseClass = 'login__form'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
import type { FormState } from 'payload'
import type { FormState, PayloadRequest } from 'payload'
import { Form, FormSubmit, PasswordField, useConfig, useTranslation } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { password } from 'payload/shared'
import type { LoginFieldProps } from '../LoginField/index.js'
@@ -81,7 +82,28 @@ export const LoginForm: React.FC<{
>
<div className={`${baseClass}__inputWrap`}>
<LoginField type={loginType} />
<PasswordField label={t('general:password')} name="password" required />
<PasswordField
autoComplete="off"
label={t('general:password')}
name="password"
required
validate={(value) =>
password(value, {
name: 'password',
type: 'text',
data: {},
preferences: { fields: {} },
req: {
payload: {
config,
},
t,
} as PayloadRequest,
required: true,
siblingData: {},
})
}
/>
</div>
<Link
href={formatAdminURL({

View File

@@ -9,6 +9,7 @@ import {
PasswordField,
useAuth,
useConfig,
useFormFields,
useTranslation,
} from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared'
@@ -63,7 +64,7 @@ export const ResetPasswordClient: React.FC<Args> = ({ token }) => {
toast.success(i18n.t('general:updatedSuccessfully'))
}
},
[adminRoute, fetchFullUser, history, i18n, loginRoute],
[fetchFullUser, history, adminRoute, i18n],
)
return (
@@ -73,15 +74,42 @@ export const ResetPasswordClient: React.FC<Args> = ({ token }) => {
method="POST"
onSuccess={onSuccess}
>
<PasswordField
label={i18n.t('authentication:newPassword')}
name="password"
path="password"
required
/>
<PasswordToConfirm />
<ConfirmPasswordField />
<HiddenField forceUsePathFromProps name="token" value={token} />
<FormSubmit>{i18n.t('authentication:resetPassword')}</FormSubmit>
</Form>
)
}
const PasswordToConfirm = () => {
const { t } = useTranslation()
const { value: confirmValue } = useFormFields(
([fields]) => (fields && fields?.['confirm-password']) || null,
)
const validate = React.useCallback(
(value: string) => {
if (!value) {
return t('validation:required')
}
if (value === confirmValue) {
return true
}
return t('fields:passwordsDoNotMatch')
},
[confirmValue, t],
)
return (
<PasswordField
autoComplete="off"
label={t('authentication:newPassword')}
name="password"
required
validate={validate}
/>
)
}

View File

@@ -1,5 +1,6 @@
import type { StepNavItem } from '@payloadcms/ui'
import type { ClientCollectionConfig, ClientGlobalConfig, FieldMap } from 'payload'
import type { FieldMap } from '@payloadcms/ui/utilities/buildComponentMap'
import type { ClientCollectionConfig, ClientGlobalConfig } from 'payload'
import type React from 'react'
import { getTranslation } from '@payloadcms/translations'

View File

@@ -81,8 +81,6 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
const canUpdate = docPermissions?.update?.permission
const localeValues = locales.map((locale) => locale.value)
return (
<main className={baseClass}>
<SetViewActions actions={componentMap?.actionsMap?.Edit?.Version} />
@@ -111,7 +109,6 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
globalSlug={globalSlug}
label={collectionConfig?.labels.singular || globalConfig?.label}
originalDocID={id}
status={doc?.version?._status}
versionDate={versionCreatedAt}
versionID={versionID}
/>
@@ -139,7 +136,11 @@ export const DefaultVersionView: React.FC<DefaultVersionsViewProps> = ({
fieldMap={fieldMap}
fieldPermissions={docPermissions?.fields}
i18n={i18n}
locales={localeValues}
locales={
locales
? locales.map(({ label }) => (typeof label === 'string' ? label : undefined))
: []
}
version={
globalConfig
? {

View File

@@ -1,4 +1,4 @@
import type { MappedField } from 'payload'
import type { MappedField } from '@payloadcms/ui/utilities/buildComponentMap'
import { getTranslation } from '@payloadcms/translations'
import { getUniqueListBy } from 'payload/shared'

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