Compare commits

..

1 Commits

Author SHA1 Message Date
Jarrod Flesch
c9d0d1ffcc clean up provider a bit 2025-07-30 21:52:48 -04:00
545 changed files with 3130 additions and 8813 deletions

View File

@@ -17,9 +17,6 @@ env:
jobs:
post_release:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-24.04
if: ${{ github.event_name != 'workflow_dispatch' }}
steps:

2
.gitignore vendored
View File

@@ -331,7 +331,5 @@ test/databaseAdapter.js
test/.localstack
test/google-cloud-storage
test/azurestoragedata/
/media-without-delete-access
licenses.csv

View File

@@ -739,7 +739,7 @@ The `useDocumentInfo` hook provides information about the current document being
| **`lastUpdateTime`** | Timestamp of the last update to the document. |
| **`mostRecentVersionIsAutosaved`** | Whether the most recent version is an autosaved version. |
| **`preferencesKey`** | The `preferences` key to use when interacting with document-level user preferences. [More details](./preferences). |
| **`data`** | The saved data of the document. |
| **`savedDocumentData`** | The saved data of the document. |
| **`setDocFieldPreferences`** | Method to set preferences for a specific field. [More details](./preferences). |
| **`setDocumentTitle`** | Method to set the document title. |
| **`setHasPublishedDoc`** | Method to update whether the document has been published. |

View File

@@ -33,7 +33,7 @@ export const Users: CollectionConfig = {
}
```
![Authentication Admin Panel functionality](https://payloadcms.com/images/docs/auth-overview.jpg)
![Authentication Admin Panel functionality](https://payloadcms.com/images/docs/auth-admin.jpg)
_Admin Panel screenshot depicting an Admins Collection with Auth enabled_
## Config Options

View File

@@ -141,8 +141,8 @@ The following options are available:
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| `components` | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
| `pagination` | Set pagination-specific options for this Collection in the List View. [More details](#pagination). |
| `baseFilter` | Defines a default base filter which will be applied to the List View (along with any other filters applied by the user) and internal links in Lexical Editor, |
| `pagination` | Set pagination-specific options for this Collection. [More details](#pagination). |
| `baseListFilter` | You can define a default base filter for this collection's List view, which will be merged into any filters that the user performs. |
<Banner type="warning">
**Note:** If you set `useAsTitle` to a relationship or join field, it will use

View File

@@ -158,7 +158,7 @@ export function MyCustomView(props: AdminViewServerProps) {
<Banner type="success">
**Tip:** For consistent layout and navigation, you may want to wrap your
Custom View with one of the built-in [Templates](./overview#templates).
Custom View with one of the built-in [Template](./overview#templates).
</Banner>
### View Templates

View File

@@ -293,6 +293,7 @@ Here's an example of a custom `editMenuItems` component:
```tsx
import React from 'react'
import { PopupList } from '@payloadcms/ui'
import type { EditMenuItemsServerProps } from 'payload'
@@ -300,12 +301,12 @@ export const EditMenuItems = async (props: EditMenuItemsServerProps) => {
const href = `/custom-action?id=${props.id}`
return (
<>
<a href={href}>Custom Edit Menu Item</a>
<a href={href}>
<PopupList.ButtonGroup>
<PopupList.Button href={href}>Custom Edit Menu Item</PopupList.Button>
<PopupList.Button href={href}>
Another Custom Edit Menu Item - add as many as you need!
</a>
</>
</PopupList.Button>
</PopupList.ButtonGroup>
)
}
```

View File

@@ -63,22 +63,3 @@ export const MyCollection: CollectionConfig = {
],
}
```
## Localized fields and MongoDB indexes
When you set `index: true` or `unique: true` on a localized field, MongoDB creates one index **per locale path** (e.g., `slug.en`, `slug.da-dk`, etc.). With many locales and indexed fields, this can quickly approach MongoDB's per-collection index limit.
If you know you'll query specifically by a locale, index only those locale paths using the collection-level `indexes` option instead of setting `index: true` on the localized field. This approach gives you more control and helps avoid unnecessary indexes.
```ts
import type { CollectionConfig } from 'payload'
export const Pages: CollectionConfig = {
fields: [{ name: 'slug', type: 'text', localized: true }],
indexes: [
// Index English slug only (rather than all locales)
{ fields: ['slug.en'] },
// You could also make it unique:
// { fields: ['slug.en'], unique: true },
],
}
```

View File

@@ -60,21 +60,21 @@ You can access Mongoose models as follows:
## Using other MongoDB implementations
You can import the `compatibilityOptions` object to get the recommended settings for other MongoDB implementations. Since these databases aren't officially supported by payload, you may still encounter issues even with these settings (please create an issue or PR if you believe these options should be updated):
You can import the `compatabilityOptions` object to get the recommended settings for other MongoDB implementations. Since these databases aren't officially supported by payload, you may still encounter issues even with these settings (please create an issue or PR if you believe these options should be updated):
```ts
import { mongooseAdapter, compatibilityOptions } from '@payloadcms/db-mongodb'
import { mongooseAdapter, compatabilityOptions } from '@payloadcms/db-mongodb'
export default buildConfig({
db: mongooseAdapter({
url: process.env.DATABASE_URI,
// For example, if you're using firestore:
...compatibilityOptions.firestore,
...compatabilityOptions.firestore,
}),
})
```
We export compatibility options for [DocumentDB](https://aws.amazon.com/documentdb/), [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db) and [Firestore](https://cloud.google.com/firestore/mongodb-compatibility/docs/overview). Known limitations:
We export compatability options for [DocumentDB](https://aws.amazon.com/documentdb/), [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db) and [Firestore](https://cloud.google.com/firestore/mongodb-compatibility/docs/overview). Known limitations:
- Azure Cosmos DB does not support transactions that update two or more documents in different collections, which is a common case when using Payload (via hooks).
- Azure Cosmos DB the root config property `indexSortableFields` must be set to `true`.

View File

@@ -296,16 +296,11 @@ query {
sort: "createdAt"
limit: 5
where: { author: { equals: "66e3431a3f23e684075aaeb9" } }
"""
Optionally pass count: true if you want to retrieve totalDocs
"""
count: true -- s
) {
docs {
title
}
hasNextPage
totalDocs
}
}
}

View File

@@ -162,11 +162,6 @@ const result = await payload.find({
})
```
<Banner type="info">
`pagination`, `page`, and `limit` are three related properties [documented
here](/docs/queries/pagination).
</Banner>
### Find by ID#collection-find-by-id
```js

View File

@@ -207,7 +207,7 @@ Everything mentioned above applies to local development as well, but there are a
### Enable Turbopack
<Banner type="warning">
**Note:** In the future this will be the default. Use at your own risk.
**Note:** In the future this will be the default. Use as your own risk.
</Banner>
Add `--turbo` to your dev script to significantly speed up your local development server start time.

View File

@@ -54,15 +54,8 @@ The plugin accepts an object with the following properties:
```ts
type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
* Base path for your application
*
* https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath
*
* @default undefined
*/
basePath?: string
/**
* After a tenant is deleted, the plugin will attempt to clean up related documents
* After a tenant is deleted, the plugin will attempt
* to clean up related documents
* - removing documents with the tenant ID
* - removing the tenant from users
*
@@ -75,36 +68,22 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
collections: {
[key in CollectionSlug]?: {
/**
* Set to `true` if you want the collection to behave as a global
* Set to `true` if you want the collection to
* behave as a global
*
* @default false
*/
isGlobal?: boolean
/**
* Overrides for the tenant field, will override the entire tenantField configuration
*/
tenantFieldOverrides?: CollectionTenantFieldConfigOverrides
/**
* Set to `false` if you want to manually apply
* the baseFilter
*
* @default true
*/
useBaseFilter?: boolean
/**
* @deprecated Use `useBaseFilter` instead. If both are defined,
* `useBaseFilter` will take precedence. This property remains only
* for backward compatibility and may be removed in a future version.
*
* Originally, `baseListFilter` was intended to filter only the List View
* in the admin panel. However, base filtering is often required in other areas
* such as internal link relationships in the Lexical editor.
* the baseListFilter
*
* @default true
*/
useBaseListFilter?: boolean
/**
* Set to `false` if you want to handle collection access manually without the multi-tenant constraints applied
* Set to `false` if you want to handle collection access
* manually without the multi-tenant constraints applied
*
* @default true
*/
@@ -113,7 +92,8 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
}
/**
* Enables debug mode
* - Makes the tenant field visible in the admin UI within applicable collections
* - Makes the tenant field visible in the
* admin UI within applicable collections
*
* @default false
*/
@@ -125,41 +105,27 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
*/
enabled?: boolean
/**
* Localization for the plugin
* Field configuration for the field added
* to all tenant enabled collections
*/
i18n?: {
translations: {
[key in AcceptedLanguages]?: {
/**
* @default 'You are about to change ownership from <0>{{fromTenant}}</0> to <0>{{toTenant}}</0>'
*/
'confirm-modal-tenant-switch--body'?: string
/**
* `tenantLabel` defaults to the value of the `nav-tenantSelector-label` translation
*
* @default 'Confirm {{tenantLabel}} change'
*/
'confirm-modal-tenant-switch--heading'?: string
/**
* @default 'Assigned Tenant'
*/
'field-assignedTenant-label'?: string
/**
* @default 'Tenant'
*/
'nav-tenantSelector-label'?: string
}
}
tenantField?: {
access?: RelationshipField['access']
/**
* The name of the field added to all tenant
* enabled collections
*
* @default 'tenant'
*/
name?: string
}
/**
* Field configuration for the field added to all tenant enabled collections
*/
tenantField?: RootTenantFieldConfigOverrides
/**
* Field configuration for the field added to the users collection
* Field configuration for the field added
* to the users collection
*
* If `includeDefaultField` is `false`, you must include the field on your users collection manually
* This is useful if you want to customize the field or place the field in a specific location
* If `includeDefaultField` is `false`, you must
* include the field on your users collection manually
* This is useful if you want to customize the field
* or place the field in a specific location
*/
tenantsArrayField?:
| {
@@ -180,7 +146,8 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
*/
arrayTenantFieldName?: string
/**
* When `includeDefaultField` is `true`, the field will be added to the users collection automatically
* When `includeDefaultField` is `true`, the field will
* be added to the users collection automatically
*/
includeDefaultField?: true
/**
@@ -197,7 +164,8 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
arrayFieldName?: string
arrayTenantFieldName?: string
/**
* When `includeDefaultField` is `false`, you must include the field on your users collection manually
* When `includeDefaultField` is `false`, you must
* include the field on your users collection manually
*/
includeDefaultField?: false
rowFields?: never
@@ -206,9 +174,8 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
* Customize tenant selector label
*
* Either a string or an object where the keys are i18n codes and the values are the string labels
*
* @deprecated Use `i18n.translations` instead.
* Either a string or an object where the keys are i18n
* codes and the values are the string labels
*/
tenantSelectorLabel?:
| Partial<{
@@ -222,25 +189,27 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
*/
tenantsSlug?: string
/**
* Function that determines if a user has access to _all_ tenants
* Function that determines if a user has access
* to _all_ tenants
*
* Useful for super-admin type users
*/
userHasAccessToAllTenants?: (
user: ConfigTypes extends { user: unknown }
? ConfigTypes['user']
: TypedUser,
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
) => boolean
/**
* Opt out of adding access constraints to the tenants collection
* Opt out of adding access constraints to
* the tenants collection
*/
useTenantsCollectionAccess?: boolean
/**
* Opt out including the baseListFilter to filter tenants by selected tenant
* Opt out including the baseListFilter to filter
* tenants by selected tenant
*/
useTenantsListFilter?: boolean
/**
* Opt out including the baseListFilter to filter users by selected tenant
* Opt out including the baseListFilter to filter
* users by selected tenant
*/
useUsersTenantFilter?: boolean
}

View File

@@ -6,112 +6,9 @@ desc: Troubleshooting Common Issues in Payload
keywords: admin, components, custom, customize, documentation, Content Management System, cms, headless, javascript, node, react, nextjs, troubleshooting
---
## Dependency mismatches
## Common Issues
All `payload` and `@payloadcms/*` packages must be on exactly the same version and installed only once.
When two copies—or two different versions—of any of these packages (or of `react` / `react-dom`) appear in your dependency graph, you can see puzzling runtime errors. The most frequent is a broken React context:
```bash
TypeError: Cannot destructure property 'config' of...
```
This happens because one package imports a hook (most commonly `useConfig`) from _version A_ while the context provider comes from _version B_. The fix is always the same: make sure every Payload-related and React package resolves to the same module.
### Confirm whether duplicates exist
The first thing to do is to confirm whether duplicative dependencies do in fact exist.
There are two ways to do this:
1. Using pnpm's built-in inspection tool
```bash
pnpm why @payloadcms/ui
```
This prints the dependency tree and shows which versions are being installed. If you see more than one distinct version—or the same version listed under different paths—you have duplication.
2. Manual check (works with any package manager)
```bash
find node_modules -name package.json \
-exec grep -H '"name": "@payloadcms/ui"' {} \;
```
Most of these hits are likely symlinks created by pnpm. Edit the matching package.json files (temporarily add a comment or change a description) to confirm whether they point to the same physical folder or to multiple copies.
Perform the same two checks for react and react-dom; a second copy of React can cause identical symptoms.
#### If no duplicates are found
`@payloadcms/ui` intentionally contains two bundles of itself, so you may see dual paths even when everything is correct. Inside the Payload Admin UI you must import only:
- `@payloadcms/ui`
- `@payloadcms/ui/rsc`
- `@payloadcms/ui/shared`
Any other deep import such as `@payloadcms/ui/elements/Button` should **only** be used in your own frontend, outside of the Payload Admin Panel. Those deep entries are published un-bundled to help you tree-shake and ship a smaller client bundle if you only need a few components from `@payloadcms/ui`.
### Fixing depedendency issues
These steps assume `pnpm`, which the Payload team recommends and uses internally. The principles apply to other package managers like npm and yarn as well. Do note that yarn 1.x is not supported by Payload.
1. Pin every critical package to an exact version
In package.json remove `^` or `~` from all versions of:
- `payload`
- `@payloadcms/*`
- `react`
- `react-dom`
Prefixes allow your package manager to float to a newer minor/patch release, causing mismatches.
2. Delete node_modules
Old packages often linger even after you change versions or removed them from your package.json. Deleting node_modules ensures a clean slate.
3. Re-install dependencies
```bash
pnpm install
```
#### If the error persists
1. Clean the global store (pnpm only)
```bash
pnpm store prune
```
2. Delete the lockfile
Depending on your package manager, this could be `pnpm-lock.yaml`, `package-lock.json`, or `yarn.lock`.
Make sure you delete the lockfile **and** the node_modules folder at the same time, then run `pnpm install`. This forces a fresh, consistent resolution for all packages. It will also update all packages with dynamic versions to the latest version.
While it's best practice to manage dependencies in such a way where the lockfile can easily be re-generated (often this is the easiest way to resolve dependency issues), this may break your project if you have not tested the latest versions of your dependencies.
If you are using a version control system, make sure to commit your lockfile after this step.
3. Deduplicate anything that slipped through
```bash
pnpm dedupe
```
**Still stuck?**
- Switch to `pnpm` if you are on npm. Its symlinked store helps reducing accidental duplication.
- Inspect the lockfile directly for peer-dependency violations.
- Check project-level .npmrc / .pnpmfile.cjs overrides.
- Run [Syncpack](https://www.npmjs.com/package/syncpack) to enforce identical versions of every `@payloadcms/*`, `react`, and `react-dom` reference.
Absolute last resort: add Webpack aliases so that all imports of a given package resolve to the same path (e.g. `resolve.alias['react'] = path.resolve('./node_modules/react')`). Keep this only until you can fix the underlying version skew.
## "Unauthorized, you must be logged in to make this request" when attempting to log in
### "Unauthorized, you must be logged in to make this request" when attempting to log in
This means that your auth cookie is not being set or accepted correctly upon logging in. To resolve check the following settings in your Payload Config:

View File

@@ -13,8 +13,8 @@ keywords: uploads, images, media, overview, documentation, Content Management Sy
</Banner>
<LightDarkImage
srcLight="https://payloadcms.com/images/docs/uploads-overview.jpg"
srcDark="https://payloadcms.com/images/docs/uploads-overview.jpg"
srcLight="https://payloadcms.com/images/docs/upload-admin.jpg"
srcDark="https://payloadcms.com/images/docs/upload-admin.jpg"
alt="Shows an Upload enabled collection in the Payload Admin Panel"
caption="Admin Panel screenshot depicting a Media Collection with Upload enabled"
/>

View File

@@ -12,7 +12,7 @@ Extending on Payload's [Draft](/docs/versions/drafts) functionality, you can con
Autosave relies on Versions and Drafts being enabled in order to function.
</Banner>
![Autosave Enabled](/images/docs/autosave-v3.jpg)
![Autosave Enabled](/images/docs/autosave-enabled.png)
_If Autosave is enabled, drafts will be created automatically as the document is modified and the Admin UI adds an indicator describing when the document was last saved to the top right of the sidebar._
## Options

View File

@@ -14,7 +14,7 @@ Payload's Draft functionality builds on top of the Versions functionality to all
By enabling Versions with Drafts, your collections and globals can maintain _newer_, and _unpublished_ versions of your documents. It's perfect for cases where you might want to work on a document, update it and save your progress, but not necessarily make it publicly published right away. Drafts are extremely helpful when building preview implementations.
![Drafts Enabled](/images/docs/autosave-drafts.jpg)
![Drafts Enabled](/images/docs/drafts-enabled.png)
_If Drafts are enabled, the typical Save button is replaced with new actions which allow you to either save a draft, or publish your changes._
## Options

View File

@@ -13,7 +13,7 @@ keywords: version history, revisions, audit log, draft, publish, restore, autosa
When enabled, Payload will automatically scaffold a new Collection in your database to store versions of your document(s) over time, and the Admin UI will be extended with additional views that allow you to browse document versions, view diffs in order to see exactly what has changed in your documents (and when they changed), and restore documents back to prior versions easily.
![Versions](/images/docs/versions-v3.jpg)
![Versions](/images/docs/versions.png)
_Comparing an old version to a newer version of a document_
**With Versions, you can:**

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.53.0",
"version": "3.49.1",
"private": true,
"type": "module",
"workspaces": [

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/admin-bar",
"version": "3.53.0",
"version": "3.49.1",
"description": "An admin bar for React apps using Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.53.0",
"version": "3.49.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

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

View File

@@ -17,16 +17,10 @@ export const create: Create = async function create(
const options: CreateOptions = {
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
let doc
if (!data.createdAt) {
data.createdAt = new Date().toISOString()
}
transform({
adapter: this,
data,

View File

@@ -14,10 +14,6 @@ export const createGlobal: CreateGlobal = async function createGlobal(
) {
const { globalConfig, Model } = getGlobal({ adapter: this, globalSlug })
if (!data.createdAt) {
;(data as any).createdAt = new Date().toISOString()
}
transform({
adapter: this,
data,
@@ -28,8 +24,6 @@ export const createGlobal: CreateGlobal = async function createGlobal(
const options: CreateOptions = {
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
let [result] = (await Model.create([data], options)) as any

View File

@@ -25,8 +25,6 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
const options = {
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
const data = {
@@ -39,9 +37,6 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
updatedAt,
version: versionData,
}
if (!data.createdAt) {
data.createdAt = new Date().toISOString()
}
const fields = buildVersionGlobalFields(this.payload.config, globalConfig)

View File

@@ -29,8 +29,6 @@ export const createVersion: CreateVersion = async function createVersion(
const options = {
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
const data = {
@@ -43,9 +41,6 @@ export const createVersion: CreateVersion = async function createVersion(
updatedAt,
version: versionData,
}
if (!data.createdAt) {
data.createdAt = new Date().toISOString()
}
const fields = buildVersionCollectionFields(this.payload.config, collectionConfig)

View File

@@ -331,7 +331,7 @@ export function mongooseAdapter({
}
}
export { compatibilityOptions } from './utilities/compatibilityOptions.js'
export { compatabilityOptions } from './utilities/compatabilityOptions.js'
/**
* Attempt to find migrations directory.

View File

@@ -63,10 +63,7 @@ const migrateModelWithBatching = async ({
},
},
})),
{
session, // Timestamps are manually added by the write transform
timestamps: false,
},
{ session },
)
skip += batchSize

View File

@@ -26,8 +26,6 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
select,
}),
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
transform({ adapter: this, data, fields, globalSlug, operation: 'write' })

View File

@@ -39,8 +39,6 @@ export async function updateGlobalVersion<T extends TypeWithID>(
select,
}),
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
const query = await buildQuery({

View File

@@ -36,8 +36,6 @@ export const updateJobs: UpdateJobs = async function updateMany(
lean: true,
new: true,
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
let query = await buildQuery({

View File

@@ -58,8 +58,6 @@ export const updateMany: UpdateMany = async function updateMany(
select,
}),
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
let query = await buildQuery({

View File

@@ -38,8 +38,6 @@ export const updateOne: UpdateOne = async function updateOne(
select,
}),
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
const query = await buildQuery({

View File

@@ -45,8 +45,6 @@ export const updateVersion: UpdateVersion = async function updateVersion(
select,
}),
session: await getSession(this, req),
// Timestamps are manually added by the write transform
timestamps: false,
}
const query = await buildQuery({

View File

@@ -2,9 +2,9 @@ import type { Args } from '../index.js'
/**
* Each key is a mongo-compatible database and the value
* is the recommended `mongooseAdapter` settings for compatibility.
* is the recommended `mongooseAdapter` settings for compatability.
*/
export const compatibilityOptions = {
export const compatabilityOptions = {
cosmosdb: {
transactionOptions: false,
useJoinAggregations: false,
@@ -12,7 +12,6 @@ export const compatibilityOptions = {
},
documentdb: {
disableIndexHints: true,
useJoinAggregations: false,
},
firestore: {
disableIndexHints: true,

View File

@@ -395,10 +395,6 @@ describe('transform', () => {
data,
fields: config.collections[0].fields,
})
if ('updatedAt' in data) {
delete data.updatedAt
}
const flattenValuesAfter = Object.values(flattenRelationshipValues(data))
flattenValuesAfter.forEach((value, i) => {

View File

@@ -548,10 +548,4 @@ export const transform = ({
parentIsLocalized,
ref: data,
})
if (operation === 'write') {
if (!data.updatedAt) {
data.updatedAt = new Date().toISOString()
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.53.0",
"version": "3.49.1",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
"version": "3.53.0",
"version": "3.49.1",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

@@ -791,14 +791,9 @@ export const traverseFields = ({
} else {
shouldSelect = true
}
const tableName = fieldShouldBeLocalized({ field, parentIsLocalized })
? `${currentTableName}${adapter.localesSuffix}`
: currentTableName
if (shouldSelect) {
args.extras[name] = sql
.raw(`ST_AsGeoJSON("${adapter.tables[tableName][name].name}")::jsonb`)
.as(name)
args.extras[name] = sql.raw(`ST_AsGeoJSON(${toSnakeCase(name)})::jsonb`).as(name)
}
break
}

View File

@@ -546,19 +546,6 @@ export const traverseFields = ({
valuesToTransform.forEach(({ localeKey, ref, value }) => {
let formattedValue = value
if (field.type === 'date') {
if (fieldName === 'updatedAt' && !formattedValue) {
// let the db handle this
formattedValue = new Date().toISOString()
} else {
if (typeof value === 'number' && !Number.isNaN(value)) {
formattedValue = new Date(value).toISOString()
} else if (value instanceof Date) {
formattedValue = value.toISOString()
}
}
}
if (typeof value !== 'undefined') {
if (value && field.type === 'point' && adapter.name !== 'sqlite') {
formattedValue = sql`ST_GeomFromGeoJSON(${JSON.stringify(value)})`
@@ -583,6 +570,19 @@ export const traverseFields = ({
formattedValue = sql.raw(`${columnName} + ${value.$inc}`)
}
if (field.type === 'date') {
if (typeof value === 'number' && !Number.isNaN(value)) {
formattedValue = new Date(value).toISOString()
} else if (value instanceof Date) {
formattedValue = value.toISOString()
}
}
}
if (field.type === 'date' && fieldName === 'updatedAt') {
// let the db handle this
formattedValue = new Date().toISOString()
}
if (typeof formattedValue !== 'undefined') {

View File

@@ -42,10 +42,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
upsertTarget,
where,
}: Args): Promise<T> => {
if (operation === 'create' && !data.createdAt) {
data.createdAt = new Date().toISOString()
}
let insertedRow: Record<string, unknown> = { id }
if (id && shouldUseOptimizedUpsertRow({ data, fields })) {
const { row } = transformForWrite({

View File

@@ -9,12 +9,7 @@ export const buildIndexName = ({
name: string
number?: number
}): string => {
let indexName = `${name}${number ? `_${number}` : ''}_idx`
if (indexName.length > 60) {
const suffix = `${number ? `_${number}` : ''}_idx`
indexName = `${name.slice(0, 60 - suffix.length)}${suffix}`
}
const indexName = `${name}${number ? `_${number}` : ''}_idx`
if (!adapter.indexes.has(indexName)) {
adapter.indexes.add(indexName)

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.53.0",
"version": "3.49.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -379,11 +379,9 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
),
},
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
totalDocs: { type: GraphQLInt },
},
}),
args: {
count: { type: GraphQLBoolean },
limit: {
type: GraphQLInt,
},
@@ -404,7 +402,7 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
},
async resolve(parent, args, context: Context) {
const { collection } = field
const { count = false, limit, page, sort, where } = args
const { limit, page, sort, where } = args
const { req } = context
const draft = Boolean(args.draft ?? context.req.query?.draft)
@@ -431,7 +429,7 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
throw new Error('GraphQL with array of join.field.collection is not implemented')
}
const { docs, totalDocs } = await req.payload.find({
const { docs } = await req.payload.find({
collection,
depth: 0,
draft,
@@ -441,7 +439,7 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
locale: req.locale,
overrideAccess: false,
page,
pagination: count ? true : false,
pagination: false,
req,
sort,
where: fullWhere,
@@ -456,7 +454,6 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
return {
docs: shouldSlice ? docs.slice(0, -1) : docs,
hasNextPage: limit === 0 ? false : limit < docs.length,
...(count ? { totalDocs } : {}),
}
},
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.53.0",
"version": "3.49.1",
"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.53.0",
"version": "3.49.1",
"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.53.0",
"version": "3.49.1",
"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.53.0",
"version": "3.49.1",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -9,7 +9,7 @@ const handlerBuilder =
async (
request: Request,
args: {
params: Promise<{ slug?: string[] }>
params: Promise<{ slug: string[] }>
},
): Promise<Response> => {
const awaitedConfig = await config

View File

@@ -1,17 +1,15 @@
@import '~@payloadcms/ui/scss';
$tab-width: 24px;
$tab-width: 16px;
@layer payload-default {
.query-inspector {
--tab-width: 24px;
&__json-children {
position: relative;
&--nested {
& li {
padding-left: 8px;
padding-left: $tab-width;
}
}
@@ -25,14 +23,6 @@ $tab-width: 24px;
}
}
&__row-line {
&--nested {
.query-inspector__json-children {
padding-left: var(--tab-width);
}
}
}
&__list-wrap {
position: relative;
}
@@ -47,16 +37,10 @@ $tab-width: 24px;
border-bottom-right-radius: 0;
position: relative;
display: flex;
column-gap: 14px;
row-gap: 10px;
gap: 10px;
align-items: center;
left: 0;
left: -3px;
width: calc(100% + 3px);
background-color: var(--theme-elevation-50);
&:not(.query-inspector__list-toggle--empty) {
margin-left: calc(var(--tab-width) * -1 - 10px);
}
svg .stroke {
stroke: var(--theme-elevation-400);
@@ -98,31 +82,13 @@ $tab-width: 24px;
&__bracket {
position: relative;
&--position-end {
left: 2px;
width: calc(100% - 5px);
&--nested {
margin-left: $tab-width;
}
}
// Some specific rules targetting the very top of the nested JSON structure or very first items since they need slightly different styling
&__results {
& > .query-inspector__row-line--nested {
& > .query-inspector__list-toggle {
margin-left: 0;
column-gap: 6px;
.query-inspector__toggle-row-icon {
margin-left: -4px;
}
}
& > .query-inspector__json-children {
padding-left: calc(var(--base) * 1);
}
& > .query-inspector__bracket--nested > .query-inspector__bracket--position-end {
padding-left: 16px;
}
&--position-end {
left: 1px;
width: calc(100% - 5px);
}
}
}

View File

@@ -333,7 +333,6 @@ export const renderDocument = async ({
}
const documentSlots = renderDocumentSlots({
id,
collectionConfig,
globalConfig,
hasSavePermission,

View File

@@ -1,5 +1,6 @@
import type {
BeforeDocumentControlsServerPropsOnly,
DefaultServerFunctionArgs,
DocumentSlots,
EditMenuItemsServerPropsOnly,
PayloadRequest,
@@ -26,11 +27,10 @@ export const renderDocumentSlots: (args: {
collectionConfig?: SanitizedCollectionConfig
globalConfig?: SanitizedGlobalConfig
hasSavePermission: boolean
id?: number | string
permissions: SanitizedDocumentPermissions
req: PayloadRequest
}) => DocumentSlots = (args) => {
const { id, collectionConfig, globalConfig, hasSavePermission, req } = args
const { collectionConfig, globalConfig, hasSavePermission, req } = args
const components: DocumentSlots = {} as DocumentSlots
@@ -39,7 +39,6 @@ export const renderDocumentSlots: (args: {
const isPreviewEnabled = collectionConfig?.admin?.preview || globalConfig?.admin?.preview
const serverProps: ServerProps = {
id,
i18n: req.i18n,
payload: req.payload,
user: req.user,
@@ -170,11 +169,10 @@ export const renderDocumentSlots: (args: {
return components
}
export const renderDocumentSlotsHandler: ServerFunction<{
collectionSlug: string
id?: number | string
}> = async (args) => {
const { id, collectionSlug, req } = args
export const renderDocumentSlotsHandler: ServerFunction<{ collectionSlug: string }> = async (
args,
) => {
const { collectionSlug, req } = args
const collectionConfig = req.payload.collections[collectionSlug]?.config
@@ -189,7 +187,6 @@ export const renderDocumentSlotsHandler: ServerFunction<{
})
return renderDocumentSlots({
id,
collectionConfig,
hasSavePermission,
permissions: docPermissions,

View File

@@ -143,7 +143,7 @@ export const handleGroupBy = async ({
},
})
let heading = valueOrRelationshipID
let heading = valueOrRelationshipID || req.i18n.t('general:noValue')
if (
groupByField?.type === 'relationship' &&
@@ -155,24 +155,14 @@ export const handleGroupBy = async ({
valueOrRelationshipID
}
if (groupByField.type === 'date' && valueOrRelationshipID) {
if (groupByField.type === 'date') {
heading = formatDate({
date: String(valueOrRelationshipID),
date: String(heading),
i18n: req.i18n,
pattern: clientConfig.admin.dateFormat,
})
}
if (groupByField.type === 'checkbox') {
if (valueOrRelationshipID === true) {
heading = req.i18n.t('general:true')
}
if (valueOrRelationshipID === false) {
heading = req.i18n.t('general:false')
}
}
if (groupData.docs && groupData.docs.length > 0) {
const { columnState: newColumnState, Table: NewTable } = renderTable({
clientCollectionConfig,
@@ -184,7 +174,7 @@ export const handleGroupBy = async ({
enableRowSelections,
groupByFieldPath,
groupByValue: valueOrRelationshipID,
heading: heading || req.i18n.t('general:noValue'),
heading,
i18n: req.i18n,
key: `table-${valueOrRelationshipID}`,
orderableFieldName: collectionConfig.orderable === true ? '_order' : undefined,

View File

@@ -138,14 +138,16 @@ export const renderListView = async (
throw new Error('not-found')
}
const baseFilterConstraint = await (
collectionConfig.admin?.baseFilter ?? collectionConfig.admin?.baseListFilter
)?.({
limit: query.limit,
page: query.page,
req,
sort: query.sort,
})
let baseListFilter = undefined
if (typeof collectionConfig.admin?.baseListFilter === 'function') {
baseListFilter = await collectionConfig.admin.baseListFilter({
limit: query.limit,
page: query.page,
req,
sort: query.sort,
})
}
let queryPreset: QueryPreset | undefined
let queryPresetPermissions: SanitizedCollectionPermission | undefined
@@ -153,7 +155,7 @@ export const renderListView = async (
let whereWithMergedSearch = mergeListSearchAndWhere({
collectionConfig,
search: typeof query?.search === 'string' ? query.search : undefined,
where: combineWhereConstraints([query?.where, baseFilterConstraint]),
where: combineWhereConstraints([query?.where, baseListFilter]),
})
if (trash === true) {

View File

@@ -26,11 +26,9 @@ export const LogoutClient: React.FC<{
const { startRouteTransition } = useRouteTransition()
const isLoggedIn = React.useMemo(() => {
return Boolean(user?.id)
}, [user?.id])
const [isLoggedOut, setIsLoggedOut] = React.useState<boolean>(!user)
const navigatingToLoginRef = React.useRef(false)
const logOutSuccessRef = React.useRef(false)
const [loginRoute] = React.useState(() =>
formatAdminURL({
@@ -47,26 +45,26 @@ export const LogoutClient: React.FC<{
const router = useRouter()
const handleLogOut = React.useCallback(async () => {
await logOut()
const loggedOut = await logOut()
setIsLoggedOut(loggedOut)
if (!inactivity && !navigatingToLoginRef.current) {
if (!inactivity && loggedOut && !logOutSuccessRef.current) {
toast.success(t('authentication:loggedOutSuccessfully'))
navigatingToLoginRef.current = true
logOutSuccessRef.current = true
startRouteTransition(() => router.push(loginRoute))
return
}
}, [inactivity, logOut, loginRoute, router, startRouteTransition, t])
useEffect(() => {
if (isLoggedIn) {
if (!isLoggedOut) {
void handleLogOut()
} else if (!navigatingToLoginRef.current) {
navigatingToLoginRef.current = true
} else {
startRouteTransition(() => router.push(loginRoute))
}
}, [handleLogOut, isLoggedIn, loginRoute, router, startRouteTransition])
}, [handleLogOut, isLoggedOut, loginRoute, router, startRouteTransition])
if (!isLoggedIn && inactivity) {
if (isLoggedOut && inactivity) {
return (
<div className={`${baseClass}__wrap`}>
<h2>{t('authentication:loggedOutInactivity')}</h2>

View File

@@ -90,7 +90,7 @@ export const SetStepNav: React.FC<{
}),
},
{
label: t('version:versions'),
label: 'Versions',
url: formatAdminURL({
adminRoute,
path: `${docBasePath}/versions`,
@@ -118,7 +118,7 @@ export const SetStepNav: React.FC<{
}),
},
{
label: t('version:versions'),
label: 'Versions',
url: formatAdminURL({
adminRoute,
path: `/globals/${globalSlug}/versions`,

View File

@@ -17,16 +17,10 @@ import {
type SanitizedFieldPermissions,
type VersionField,
} from 'payload'
import {
fieldIsID,
fieldShouldBeLocalized,
getFieldPaths,
getFieldPermissions,
getUniqueListBy,
tabHasName,
} from 'payload/shared'
import { fieldIsID, fieldShouldBeLocalized, getUniqueListBy, tabHasName } from 'payload/shared'
import { diffComponents } from './fields/index.js'
import { getFieldPathsModified } from './utilities/getFieldPathsModified.js'
export type BuildVersionFieldsArgs = {
clientSchemaMap: ClientFieldSchemaMap
@@ -90,7 +84,7 @@ export const buildVersionFields = ({
continue
}
const { indexPath, path, schemaPath } = getFieldPaths({
const { indexPath, path, schemaPath } = getFieldPathsModified({
field,
index: fieldIndex,
parentIndexPath,
@@ -229,16 +223,21 @@ const buildVersionField = ({
BuildVersionFieldsArgs,
'fields' | 'parentIndexPath' | 'versionFromSiblingData' | 'versionToSiblingData'
>): BaseVersionField | null => {
const { permissions, read: hasReadPermission } = getFieldPermissions({
field,
operation: 'read',
parentName: parentPath?.includes('.')
? parentPath.split('.')[parentPath.split('.').length - 1]
: parentPath,
permissions: fieldPermissions,
})
const fieldName: null | string = 'name' in field ? field.name : null
if (!hasReadPermission) {
const hasPermission =
fieldPermissions === true ||
!fieldName ||
fieldPermissions?.[fieldName] === true ||
fieldPermissions?.[fieldName]?.read
const subFieldPermissions =
fieldPermissions === true ||
!fieldName ||
fieldPermissions?.[fieldName] === true ||
fieldPermissions?.[fieldName]?.fields
if (!hasPermission) {
return null
}
@@ -286,54 +285,34 @@ const buildVersionField = ({
indexPath: tabIndexPath,
path: tabPath,
schemaPath: tabSchemaPath,
} = getFieldPaths({
} = getFieldPathsModified({
field: tabAsField,
index: tabIndex,
parentIndexPath: indexPath,
parentPath,
parentSchemaPath,
})
let tabPermissions: typeof fieldPermissions = undefined
if (typeof permissions === 'boolean') {
tabPermissions = permissions
} else if (permissions && typeof permissions === 'object') {
if ('name' in tab) {
tabPermissions =
typeof permissions.fields?.[tab.name] === 'object'
? permissions.fields?.[tab.name].fields
: permissions.fields?.[tab.name]
} else {
tabPermissions = permissions.fields
}
}
const tabVersion = {
name: 'name' in tab ? tab.name : null,
fields: buildVersionFields({
clientSchemaMap,
customDiffComponents,
entitySlug,
fieldPermissions: tabPermissions,
fieldPermissions,
fields: tab.fields,
i18n,
modifiedOnly,
nestingLevel: nestingLevel + 1,
parentIndexPath: isNamedTab ? '' : tabIndexPath,
parentIsLocalized: parentIsLocalized || tab.localized,
parentPath: isNamedTab ? tabPath : 'name' in field ? path : parentPath,
parentSchemaPath: isNamedTab
? tabSchemaPath
: 'name' in field
? schemaPath
: parentSchemaPath,
parentPath: isNamedTab ? tabPath : path,
parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath,
req,
selectedLocales,
versionFromSiblingData: 'name' in tab ? valueFrom?.[tab.name] : valueFrom,
versionToSiblingData: 'name' in tab ? valueTo?.[tab.name] : valueTo,
}).versionFields,
label: typeof tab.label === 'function' ? tab.label({ i18n, t: i18n.t }) : tab.label,
label: tab.label,
}
if (tabVersion?.fields?.length) {
baseVersionField.tabs.push(tabVersion)
@@ -345,13 +324,6 @@ const buildVersionField = ({
}
} // At this point, we are dealing with a `row`, `collapsible`, etc
else if ('fields' in field) {
let subfieldPermissions: typeof fieldPermissions = undefined
if (typeof permissions === 'boolean') {
subfieldPermissions = permissions
} else if (permissions && typeof permissions === 'object') {
subfieldPermissions = permissions.fields
}
if (field.type === 'array' && (valueTo || valueFrom)) {
const maxLength = Math.max(
Array.isArray(valueTo) ? valueTo.length : 0,
@@ -367,15 +339,15 @@ const buildVersionField = ({
clientSchemaMap,
customDiffComponents,
entitySlug,
fieldPermissions: subfieldPermissions,
fieldPermissions,
fields: field.fields,
i18n,
modifiedOnly,
nestingLevel: nestingLevel + 1,
parentIndexPath: 'name' in field ? '' : indexPath,
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: ('name' in field ? path : parentPath) + '.' + i,
parentSchemaPath: 'name' in field ? schemaPath : parentSchemaPath,
parentPath: path + '.' + i,
parentSchemaPath: schemaPath,
req,
selectedLocales,
versionFromSiblingData: fromRow,
@@ -391,7 +363,7 @@ const buildVersionField = ({
clientSchemaMap,
customDiffComponents,
entitySlug,
fieldPermissions: subfieldPermissions,
fieldPermissions,
fields: field.fields,
i18n,
modifiedOnly,
@@ -449,32 +421,19 @@ const buildVersionField = ({
}
}
let blockPermissions: typeof fieldPermissions = undefined
if (permissions === true) {
blockPermissions = true
} else {
const permissionsBlockSpecific = permissions?.blocks?.[blockSlugToMatch]
if (permissionsBlockSpecific === true) {
blockPermissions = true
} else {
blockPermissions = permissionsBlockSpecific?.fields
}
}
baseVersionField.rows[i] = buildVersionFields({
clientSchemaMap,
customDiffComponents,
entitySlug,
fieldPermissions: blockPermissions,
fieldPermissions,
fields,
i18n,
modifiedOnly,
nestingLevel: nestingLevel + 1,
parentIndexPath: 'name' in field ? '' : indexPath,
parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized),
parentPath: ('name' in field ? path : parentPath) + '.' + i,
parentSchemaPath: ('name' in field ? schemaPath : parentSchemaPath) + '.' + toBlock.slug,
parentPath: path + '.' + i,
parentSchemaPath: schemaPath + '.' + toBlock.slug,
req,
selectedLocales,
versionFromSiblingData: fromRow,
@@ -500,7 +459,7 @@ const buildVersionField = ({
*/
diffMethod: 'diffWordsWithSpace',
field: clientField,
fieldPermissions: typeof permissions === 'object' ? permissions.fields : permissions,
fieldPermissions: subFieldPermissions,
parentIsLocalized,
nestingLevel: nestingLevel ? nestingLevel : undefined,

View File

@@ -25,7 +25,7 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
parentIsLocalized,
versionValue: valueTo,
}) => {
const { i18n, t } = useTranslation()
const { i18n } = useTranslation()
const { selectedLocales } = useSelectedLocales()
const { config } = useConfig()
@@ -73,9 +73,7 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
})
const rowNumber = String(i + 1).padStart(2, '0')
const rowLabel = fieldIsArrayType(field)
? `${t('general:item')} ${rowNumber}`
: `${t('fields:block')} ${rowNumber}`
const rowLabel = fieldIsArrayType(field) ? `Item ${rowNumber}` : `Block ${rowNumber}`
return (
<div className={`${baseClass}__row`} key={i}>

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/payload-cloud",
"version": "3.53.0",
"version": "3.49.1",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.53.0",
"version": "3.49.1",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",

View File

@@ -25,7 +25,7 @@ type SelectFieldBaseClientProps = {
readonly onChange?: (e: string | string[]) => void
readonly path: string
readonly validate?: SelectFieldValidation
readonly value?: string | string[]
readonly value?: string
}
type SelectFieldBaseServerProps = Pick<FieldPaths, 'path'>

View File

@@ -56,12 +56,6 @@ export type FieldState = {
fieldSchema?: Field
filterOptions?: FilterOptionsResult
initialValue?: unknown
/**
* @experimental - Note: this property is experimental and may change in the future. Use at your own discretion.
* Every time a field is changed locally, this flag is set to true. Prevents form state from server from overwriting local changes.
* After merging server form state, this flag is reset.
*/
isModified?: boolean
/**
* The path of the field when its custom components were last rendered.
* This is used to denote if a field has been rendered, and if so,
@@ -120,11 +114,9 @@ export type BuildFormStateArgs = {
mockRSCs?: boolean
operation?: 'create' | 'update'
readOnly?: boolean
/**
* If true, will render field components within their state object.
* Performance optimization: Setting to `false` ensures that only fields that have changed paths will re-render, e.g. new array rows, etc.
* For example, you only need to render ALL fields on initial render, not on every onChange.
*/
/*
If true, will render field components within their state object
*/
renderAllFields?: boolean
req: PayloadRequest
returnLockStatus?: boolean

View File

@@ -1,3 +1,5 @@
import { v4 as uuid } from 'uuid'
import type {
AuthOperationsFromCollectionSlug,
Collection,
@@ -22,7 +24,7 @@ import { getFieldsToSign } from '../getFieldsToSign.js'
import { getLoginOptions } from '../getLoginOptions.js'
import { isUserLocked } from '../isUserLocked.js'
import { jwtSign } from '../jwt.js'
import { addSessionToUser } from '../sessions.js'
import { removeExpiredSessions } from '../removeExpiredSessions.js'
import { authenticateLocalStrategy } from '../strategies/local/authenticate.js'
import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts.js'
import { resetLoginAttempts } from '../strategies/local/resetLoginAttempts.js'
@@ -283,15 +285,34 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
user,
}
const { sid } = await addSessionToUser({
collectionConfig,
payload,
req,
user,
})
if (collectionConfig.auth.useSessions) {
// Add session to user
const newSessionID = uuid()
const now = new Date()
const tokenExpInMs = collectionConfig.auth.tokenExpiration * 1000
const expiresAt = new Date(now.getTime() + tokenExpInMs)
if (sid) {
fieldsToSignArgs.sid = sid
const session = { id: newSessionID, createdAt: now, expiresAt }
if (!user.sessions?.length) {
user.sessions = [session]
} else {
user.sessions = removeExpiredSessions(user.sessions)
user.sessions.push(session)
}
await payload.db.updateOne({
id: user.id,
collection: collectionConfig.slug,
data: user,
req,
returning: false,
})
user.collection = collectionConfig.slug
user._strategy = 'local-jwt'
fieldsToSignArgs.sid = newSessionID
}
const fieldsToSign = getFieldsToSign(fieldsToSignArgs)

View File

@@ -73,9 +73,6 @@ export const logoutOperation = async (incomingArgs: Arguments): Promise<boolean>
userWithSessions.sessions = sessionsAfterLogout
}
// Ensure updatedAt date is always updated
;(userWithSessions as any).updatedAt = new Date().toISOString()
await req.payload.db.updateOne({
id: user.id,
collection: collectionConfig.slug,

View File

@@ -10,7 +10,7 @@ import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { getFieldsToSign } from '../getFieldsToSign.js'
import { jwtSign } from '../jwt.js'
import { removeExpiredSessions } from '../sessions.js'
import { removeExpiredSessions } from '../removeExpiredSessions.js'
export type Result = {
exp: number
@@ -92,9 +92,6 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
const tokenExpInMs = collectionConfig.auth.tokenExpiration * 1000
existingSession.expiresAt = new Date(now.getTime() + tokenExpInMs)
// Ensure updatedAt date is always updated
user.updatedAt = new Date().toISOString()
await req.payload.db.updateOne({
id: user.id,
collection: collectionConfig.slug,

View File

@@ -131,9 +131,6 @@ export const resetPasswordOperation = async <TSlug extends CollectionSlug>(
// Update new password
// /////////////////////////////////////
// Ensure updatedAt date is always updated
user.updatedAt = new Date().toISOString()
const doc = await payload.db.updateOne({
id: user.id,
collection: collectionConfig.slug,

View File

@@ -46,9 +46,6 @@ export const verifyEmailOperation = async (args: Args): Promise<boolean> => {
throw new APIError('Verification token is invalid.', httpStatus.FORBIDDEN)
}
// Ensure updatedAt date is always updated
user.updatedAt = new Date().toISOString()
await req.payload.db.updateOne({
id: user.id,
collection: collection.config.slug,

View File

@@ -0,0 +1,10 @@
import type { UserSession } from './types.js'
export const removeExpiredSessions = (sessions: UserSession[]) => {
const now = new Date()
return sessions.filter(({ expiresAt }) => {
const expiry = expiresAt instanceof Date ? expiresAt : new Date(expiresAt)
return expiry > now
})
}

View File

@@ -1,70 +0,0 @@
import { v4 as uuid } from 'uuid'
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
import type { TypedUser } from '../index.js'
import type { Payload, PayloadRequest } from '../types/index.js'
import type { UserSession } from './types.js'
/**
* Removes expired sessions from an array of sessions
*/
export const removeExpiredSessions = (sessions: UserSession[]) => {
const now = new Date()
return sessions.filter(({ expiresAt }) => {
const expiry = expiresAt instanceof Date ? expiresAt : new Date(expiresAt)
return expiry > now
})
}
/**
* Adds a session to the user and removes expired sessions
* @returns The session ID (sid) if sessions are used
*/
export const addSessionToUser = async ({
collectionConfig,
payload,
req,
user,
}: {
collectionConfig: SanitizedCollectionConfig
payload: Payload
req: PayloadRequest
user: TypedUser
}): Promise<{ sid?: string }> => {
let sid: string | undefined
if (collectionConfig.auth.useSessions) {
// Add session to user
sid = uuid()
const now = new Date()
const tokenExpInMs = collectionConfig.auth.tokenExpiration * 1000
const expiresAt = new Date(now.getTime() + tokenExpInMs)
const session = { id: sid, createdAt: now, expiresAt }
if (!user.sessions?.length) {
user.sessions = [session]
} else {
user.sessions = removeExpiredSessions(user.sessions)
user.sessions.push(session)
}
// Ensure updatedAt date is always updated
user.updatedAt = new Date().toISOString()
await payload.db.updateOne({
id: user.id,
collection: collectionConfig.slug,
data: user,
req,
returning: false,
})
user.collection = collectionConfig.slug
user._strategy = 'local-jwt'
}
return {
sid,
}
}

View File

@@ -142,9 +142,6 @@ export const incrementLoginAttempts = async ({
user.sessions = currentUser.sessions
// Ensure updatedAt date is always updated
user.updatedAt = new Date().toISOString()
await payload.db.updateOne({
id: user.id,
collection: collection.slug,

View File

@@ -29,7 +29,7 @@ export type ServerOnlyCollectionProperties = keyof Pick<
export type ServerOnlyCollectionAdminProperties = keyof Pick<
SanitizedCollectionConfig['admin'],
'baseFilter' | 'baseListFilter' | 'components' | 'hidden'
'baseListFilter' | 'components' | 'hidden'
>
export type ServerOnlyUploadProperties = keyof Pick<
@@ -94,7 +94,6 @@ const serverOnlyUploadProperties: Partial<ServerOnlyUploadProperties>[] = [
const serverOnlyCollectionAdminProperties: Partial<ServerOnlyCollectionAdminProperties>[] = [
'hidden',
'baseFilter',
'baseListFilter',
'components',
// 'preview' is handled separately

View File

@@ -270,7 +270,7 @@ export type EnableFoldersOptions = {
debug?: boolean
}
export type BaseFilter = (args: {
export type BaseListFilter = (args: {
limit: number
locale?: TypedLocale
page: number
@@ -278,31 +278,7 @@ export type BaseFilter = (args: {
sort: string
}) => null | Promise<null | Where> | Where
/**
* @deprecated Use `BaseFilter` instead.
*/
export type BaseListFilter = BaseFilter
export type CollectionAdminOptions = {
/**
* Defines a default base filter which will be applied in the following parts of the admin panel:
* - List View
* - Relationship fields for internal links within the Lexical editor
*
* This is especially useful for plugins like multi-tenant. For example,
* a user may have access to multiple tenants, but should only see content
* related to the currently active or selected tenant in those places.
*/
baseFilter?: BaseFilter
/**
* @deprecated Use `baseFilter` instead. If both are defined,
* `baseFilter` will take precedence. This property remains only
* for backward compatibility and may be removed in a future version.
*
* Originally, `baseListFilter` was intended to filter only the List View
* in the admin panel. However, base filtering is often required in other areas
* such as internal link relationships in the Lexical editor.
*/
baseListFilter?: BaseListFilter
/**
* Custom admin components
@@ -712,7 +688,7 @@ export type AuthCollection = {
}
export type TypeWithID = {
deletedAt?: null | string
deletedAt?: string
docId?: any
id: number | string
}
@@ -720,7 +696,7 @@ export type TypeWithID = {
export type TypeWithTimestamps = {
[key: string]: unknown
createdAt: string
deletedAt?: null | string
deletedAt?: string
id: number | string
updatedAt: string
}

View File

@@ -32,7 +32,7 @@ export type Options<TSlug extends CollectionSlug> = {
locale?: TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -32,7 +32,7 @@ export type Options<TSlug extends CollectionSlug> = {
locale?: TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -81,7 +81,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
locale?: TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -46,7 +46,7 @@ export type BaseOptions<TSlug extends CollectionSlug, TSelect extends SelectType
locale?: TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -62,7 +62,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
locale?: TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -76,7 +76,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
locale?: 'all' | TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -77,7 +77,7 @@ export type Options<
locale?: 'all' | TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -54,7 +54,7 @@ export type Options<
locale?: 'all' | TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-restricted-exports */
import type { CollectionSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
import type { Document, PayloadRequest, PopulateType, SelectType } from '../../../types/index.js'
import type { CreateLocalReqOptions } from '../../../utilities/createLocalReq.js'
@@ -47,7 +48,7 @@ export type Options<TSlug extends CollectionSlug> = {
locale?: 'all' | TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-restricted-exports */
import type { PaginatedDocs } from '../../../database/types.js'
import type { CollectionSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
import type {
@@ -52,7 +53,7 @@ export type Options<TSlug extends CollectionSlug> = {
locale?: 'all' | TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -41,7 +41,7 @@ export type Options<TSlug extends CollectionSlug> = {
locale?: TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -76,7 +76,7 @@ export type BaseOptions<TSlug extends CollectionSlug, TSelect extends SelectType
locale?: TypedLocale
/**
* Skip access control.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
* @default true
*/
overrideAccess?: boolean

View File

@@ -258,8 +258,6 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
select: incomingSelect,
})
// Ensure updatedAt date is always updated
result.updatedAt = new Date().toISOString()
result = await req.payload.db.updateOne({
id: parentDocID,
collection: collectionConfig.slug,

View File

@@ -293,8 +293,6 @@ export const updateDocument = async <
// /////////////////////////////////////
if (!shouldSaveDraft) {
// Ensure updatedAt date is always updated
dataToUpdate.updatedAt = new Date().toISOString()
result = await req.payload.db.updateOne({
id,
collection: collectionConfig.slug,

View File

@@ -402,7 +402,6 @@ export type Params = { [key: string]: string | string[] | undefined }
export type ServerProps = {
readonly documentSubViewType?: DocumentSubViewTypes
readonly i18n: I18nClient
readonly id?: number | string
readonly locale?: Locale
readonly params?: Params
readonly payload: Payload

View File

@@ -162,12 +162,7 @@ export async function validateSearchParam({
if (versionFields) {
fieldAccess = policies[entityType]![entitySlug]!.fields
if (
segments[0] === 'parent' ||
segments[0] === 'version' ||
segments[0] === 'snapshot' ||
segments[0] === 'latest'
) {
if (segments[0] === 'parent' || segments[0] === 'version' || segments[0] === 'snapshot') {
segments.shift()
}
} else {

View File

@@ -1 +0,0 @@
export { is } from '@payloadcms/translations/languages/is'

View File

@@ -6,7 +6,6 @@ export {
parseCookies,
} from '../auth/cookies.js'
export { getLoginOptions } from '../auth/getLoginOptions.js'
export { addSessionToUser, removeExpiredSessions } from '../auth/sessions.js'
export { getFromImportMap } from '../bin/generateImportMap/utilities/getFromImportMap.js'
export { parsePayloadComponent } from '../bin/generateImportMap/utilities/parsePayloadComponent.js'
export { defaults as collectionDefaults } from '../collections/config/defaults.js'

View File

@@ -2,7 +2,8 @@ import ObjectIdImport from 'bson-objectid'
import type { TextField } from '../config/types.js'
const ObjectId = 'default' in ObjectIdImport ? ObjectIdImport.default : ObjectIdImport
const ObjectId = (ObjectIdImport.default ||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
export const baseIDField: TextField = {
name: 'id',
@@ -13,7 +14,6 @@ export const baseIDField: TextField = {
defaultValue: () => new ObjectId().toHexString(),
hooks: {
beforeChange: [({ value }) => value || new ObjectId().toHexString()],
// ID field values for arrays and blocks need to be unique when duplicating, as on postgres they are stored on the same table as primary keys.
beforeDuplicate: [() => new ObjectId().toHexString()],
},
label: 'ID',

View File

@@ -49,9 +49,6 @@ export function getFieldPaths({
}
}
/**
* @deprecated - will be removed in 4.0. Use `getFieldPaths` instead.
*/
export function getFieldPathsModified({
field,
index,

View File

@@ -111,14 +111,13 @@ export const promise = async ({
parentSchemaPath,
})
const fieldAffectsDataResult = fieldAffectsData(field)
const pathSegments = path ? path.split('.') : []
const schemaPathSegments = schemaPath ? schemaPath.split('.') : []
const indexPathSegments = indexPath ? indexPath.split('-').filter(Boolean)?.map(Number) : []
let removedFieldValue = false
if (
fieldAffectsDataResult &&
fieldAffectsData(field) &&
field.hidden &&
typeof siblingDoc[field.name!] !== 'undefined' &&
!showHiddenFields
@@ -140,17 +139,16 @@ export const promise = async ({
}
}
const shouldHoistLocalizedValue: boolean = Boolean(
const shouldHoistLocalizedValue =
flattenLocales &&
fieldAffectsDataResult &&
typeof siblingDoc[field.name!] === 'object' &&
siblingDoc[field.name!] !== null &&
fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! }) &&
locale !== 'all' &&
req.payload.config.localization,
)
fieldAffectsData(field) &&
typeof siblingDoc[field.name!] === 'object' &&
siblingDoc[field.name!] !== null &&
fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! }) &&
locale !== 'all' &&
req.payload.config.localization
if (fieldAffectsDataResult && shouldHoistLocalizedValue) {
if (shouldHoistLocalizedValue) {
// replace actual value with localized value before sanitizing
// { [locale]: fields } -> fields
const value = siblingDoc[field.name!][locale!]
@@ -189,7 +187,7 @@ export const promise = async ({
case 'group': {
// Fill groups with empty objects so fields with hooks within groups can populate
// themselves virtually as necessary
if (fieldAffectsDataResult && typeof siblingDoc[field.name] === 'undefined') {
if (fieldAffectsData(field) && typeof siblingDoc[field.name] === 'undefined') {
siblingDoc[field.name] = {}
}
@@ -236,7 +234,7 @@ export const promise = async ({
}
}
if (fieldAffectsDataResult) {
if (fieldAffectsData(field)) {
// Execute hooks
if (triggerHooks && field.hooks?.afterRead) {
for (const hook of field.hooks.afterRead) {
@@ -402,7 +400,7 @@ export const promise = async ({
}
}
if (Array.isArray(rows) && rows.length > 0) {
if (Array.isArray(rows)) {
rows.forEach((row, rowIndex) => {
traverseFields({
blockData,
@@ -470,8 +468,6 @@ export const promise = async ({
})
}
})
} else if (shouldHoistLocalizedValue && (!rows || rows.length === 0)) {
siblingDoc[field.name] = null
} else if (field.hidden !== true || showHiddenFields === true) {
siblingDoc[field.name] = []
}
@@ -481,7 +477,7 @@ export const promise = async ({
case 'blocks': {
const rows = siblingDoc[field.name]
if (Array.isArray(rows) && rows.length > 0) {
if (Array.isArray(rows)) {
rows.forEach((row, rowIndex) => {
const blockTypeToMatch = (row as JsonObject).blockType
@@ -577,8 +573,6 @@ export const promise = async ({
})
}
})
} else if (shouldHoistLocalizedValue && (!rows || rows.length === 0)) {
siblingDoc[field.name] = null
} else if (field.hidden !== true || showHiddenFields === true) {
siblingDoc[field.name] = []
}
@@ -623,7 +617,7 @@ export const promise = async ({
}
case 'group': {
if (fieldAffectsDataResult) {
if (fieldAffectsData(field)) {
let groupDoc = siblingDoc[field.name] as JsonObject
if (typeof siblingDoc[field.name] !== 'object') {

View File

@@ -63,8 +63,7 @@ export const promise = async <T>({
let fieldData = siblingDoc?.[field.name!]
const fieldIsLocalized = localization && fieldShouldBeLocalized({ field, parentIsLocalized })
// Run field beforeDuplicate hooks.
// These hooks are responsible for resetting the `id` field values of array and block rows. See `baseIDField`.
// Run field beforeDuplicate hooks
if (Array.isArray(field.hooks?.beforeDuplicate)) {
if (fieldIsLocalized) {
const localeData: JsonObject = {}

View File

@@ -1,7 +1,8 @@
import Ajv from 'ajv'
import ObjectIdImport from 'bson-objectid'
const ObjectId = 'default' in ObjectIdImport ? ObjectIdImport.default : ObjectIdImport
const ObjectId = (ObjectIdImport.default ||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
import type { TFunction } from '@payloadcms/translations'
import type { JSONSchema4 } from 'json-schema'

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