### What?
`payload.db.updateOne` (and so `payload.db.upsert`) with drizzle
adapters used incoming `where` incorrectly and worked properly only
either if you passed `id` or some where query path required table joins
(like `where: { 'array.title'`) which is also the reason why `upsert`
_worked_ with user preferences specifically, because we need to join the
`preferences_rels` table to query by `user.relationTo` and `user.value`
Fixes https://github.com/payloadcms/payload/issues/9915
This was found here - https://github.com/payloadcms/payload/pull/9913,
the database KV adapter uses `upsert` with `where` by unique fields.
### What?
Previously, the `admin.group` property on `collection` / `global`
configs allowed for a custom group and the `admin.hidden` property would
not only hide the entity from the nav sidebar / dashboard but also
disable its routes.
### Why?
There was not a simple way to hide an entity from the nav sidebar /
dashboard but still keep the entities routes.
### How?
Now - we've added the `false` type to the `admin.group` field to account
for this.
Passing `false` to `admin.group` will hide the entity from the sidebar
nav and dashboard but keep the routes available to navigate.
I.e
```
admin: {
group: false,
},
```
Fixes https://github.com/payloadcms/payload/issues/9895
We were still including field custom components in the ClientConfig,
which will throw an error if actual server-only properties were passed
to `PayloadComponent.serverProps`. This PR removes them from the
ClientConfig
This PR adds a feature which fixes another issue with migrations in
Postgres and does few refactors that significantly reduce code
duplication.
Previously, if you needed to use the underlying database directly in
migrations with the active transaction (for example to execute raw SQL),
created from `payload create:migration`, as `req` doesn't work there you
had to do something like this:
```ts
// Postgres
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
const db = payload.db.sessions?.[await req.transactionID!].db ?? payload.db
const { rows: posts } = await db.execute(sql`SELECT * from posts`)
}
// MongoDB
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
const session = payload.db.sessions?.[await req.transactionID!]
const posts = await payload.db.collections.posts.collection.find({ session }).toArray()
}
```
Which was:
1. Awkward to write
2. Not documented anywhere
Now, we expose `session` and `db` to `up` and `down` functions for you:
#### MongoDB:
```ts
import { type MigrateUpArgs } from '@payloadcms/db-mongodb'
export async function up({ session, payload, req }: MigrateUpArgs): Promise<void> {
const posts = await payload.db.collections.posts.collection.find({ session }).toArray()
}
```
#### Postgres:
```ts
import { type MigrateUpArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
const { rows: posts } = await db.execute(sql`SELECT * from posts`)
}
```
#### SQLite:
```ts
import { type MigrateUpArgs, sql } from '@payloadcms/db-sqlite'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
const { rows: posts } = await db.run(sql`SELECT * from posts`)
}
```
This actually was a thing with Postgres migrations, we already were
passing `db`, but:
1. Only for `up` and when running `payload migrate`, not for example
with `payload migrate:fresh`
2. Not documented neither in TypeScript or docs.
By ensuring we use `db`, this also fixes an issue that affects all
Postgres/SQLite migrations:
Currently, if we run `payload migration:create` with the postgres
adapter we get a file like this:
```ts
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
await payload.db.drizzle.execute(sql`
CREATE TABLE IF NOT EXISTS "users" (
"id" serial PRIMARY KEY NOT NULL,
);
```
Looks good?
Not exactly!
`payload.db.drizzle.execute()` doesn't really use the current
transaction which can lead to some problems.
Instead, it should use the `db` from `payload.db.sessions?.[await
req.transactionID!].db` because that's where we store our Drizzle
instance with the transaction.
But now, if we generate `payload migrate:create` we get:
```ts
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "users" (
"id" serial PRIMARY KEY NOT NULL,
);
```
Which is what we want, as the `db` is passed correctly here:
76428373e4/packages/drizzle/src/migrate.ts (L88-L90)
```ts
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
const dbWithTransaction = payload.db.sessions?.[await req.transactionID!].db
payload.logger.info({ one: db === dbWithTransaction })
payload.logger.info({ two: db === payload.db.drizzle })
```
<img width="336" alt="image"
src="https://github.com/user-attachments/assets/f9fab5a9-44c2-44a9-95dd-8e5cf267f027">
Additionally, this PR refactors:
* `createMigration` with Drizzle - now we have sharable
`buildCreateMigration` in `@payloadcms/drizzle` to reduce copy-pasting
of the same logic.
* the `v2-v3` relationships migration for Postgres is now shared between
`db-postgres` and `db-vercel-postgres`, again to reduce copy-paste.
This PR threads default `serverProps` to Edit and List view action slots, as well as other various components that were missing them.
---------
Co-authored-by: Alessio Gravili <alessio@gravili.de>
Previously, Autosave could trigger 2 parallel fetches where the second
could outpace the first, leading to inconsistent results.
Now, we use a simple queue-based system where we can push multiple
autosave events into a queue, and only the latest autosave will be
performed.
This also prevents multiple autosaves from ever running in parallel.
### What?
There are scenarios where the server-rendered HTML might intentionally
differ from the client-rendered DOM causing `Hydration` errors in the
DOM.
### How?
Added a new prop to the payload config `admin` object called
`suppressHydrationWarning` that allows control to display these warnings
or not.
If you set `suppressHydrationWarning` to `true`, React will not warn you
about mismatches in the attributes and the content of that element.
Defaults to `false` - so if there is a mismatch and this prop is not
defined in your config, the hydration errors will show.
```
admin: {
suppressHydrationWarning: true // will suppress the errors if there is a mismatch
}
```
The logic for creating a timestamp for use in resetPassword was not
correctly returning a valid date.
---------
Co-authored-by: Patrik Kozak <patrik@payloadcms.com>
Continuation of #9846 and partial fix for #9774. When setting
`admin.disableListFilter` retroactively, it remains active within the
list filter controls. Same for when the URL search query contains one of
these fields, except this will actually display the _wrong_ field,
falling back to the _first_ field from the config. The fix is to
properly disable the condition for this field if it's an active filter,
while still preventing it from ever rendering as an option within the
field selector itself.
Partial fix for #9774. When `admin.disableListColumn` is set
retroactively, it continues to appear in column state, but shouldn't.
This was because the table column context was not refreshing after HMR
runs, and would instead hold onto these stale columns until the page
itself refreshes. Similarly, this was also a problem when the user had
saved any of these columns to their list preferences, where those prefs
would take precedence despite these properties being set on the
underlying fields. The fix is to filter these columns from all requests
that send them, and ensure local component state properly refreshes
itself.
### What?
It became possible for fields to reset to a defined `defaultValue` when
bulk editing from the `edit-many` drawer.
### Why?
The form-state of all fields were being considered during a bulk edit -
this also meant using their initial states - this meant any fields with
default values or nested fields (`arrays`) would be overwritten with
their initial states
I.e. empty values or default values.
### How?
Now - we only send through the form data of the fields specifically
being edited in the edit-many drawer and ignore all other fields.
Leaving all other fields stay their current values.
Fixes#9590
---------
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
### What?
Custom auth collections default `useAsTitle` to `id`.
### Why?
It is more expected for auth collections to search on email or username.
### How?
Defaults useAsTitle to `username` if loginWithUsername is used, else
`email`. Can still be overridden by setting a custom `admin.useAsTitle`
property.
Fixes#9830. Continuation of #9755 and #9746. Instead of automatically
appending TLDs to the `admin.preview` and the `livePreview.url` URLs, we
should instead ensure that `req` is passed through these functions, so
that you can have full control over the format of this URL without
Payload imposing any of its own formatting.
### What?
`@lexical-html` is missing from the `lexical-proxy` exports.
### Why?
To allow `@lexical-html` functionality to be used without needing to
install the package separately.
### How?
Adds `@lexical-html` to the `lexical-proxy` exports.
Fixes#9792
### What?
Previously, the create-payload-app install would properly update the env
vars in the new created `.env` file but kept outdated env vars in the
`.env.example` file.
I.e
If selecting a `postgres` DB for the blank or website template:
`.env` --> `DATABASE_URI` -->
`postgres://postgres:<password>@127.0.0.1:5432/test-cpa`
`.env.example` --> `DATABASE_URI` -->
`mongodb://127.0.0.1/payload-template-blank-3-0`
### Now
`.env` --> `DATABASE_URI` -->
`postgres://postgres:<password>@127.0.0.1:5432/test-cpa`
`.env.example` --> `DATABASE_URI` -->
`postgres://postgres:<password>@127.0.0.1:5432/test-cpa`
### What?
* Exposes to `payload` these functions: `sanitizeSelectParam`,
`sanitizePopulateParam`, `senitizeJoinParams`.
* Refactors `sanitizeSelect` and `sanitizePopulate` to
`sanitizeSelectParam` and `sanitizePopulateParam` for clarity.
* Moves them from `@payloadcms/next` to `payload` as they aren't related
to next.
### Why?
To use these functions externally, for example in custom endpoints.
### What?
Previously, `initCollapsed: true` `array` fields would auto collapse
when typing in their respective inputs while in the create new view.
### Why?
This was due to the fact that we were only checking if `preferences`
existed in `form state` to handle the current state of the array row and
then falling back on the `initCollapsed` prop if `preferences` didn't
exist.
This was a problem because during create - `preferences` do not exist
yet. As a result, the state of the array row would keep falling back to
collapsed if `initCollapsed` was set to `true`.
### How?
To fix this, we now check the actual form state first before falling
back to preferences and then falling back to the initCollapsed prop
value.
Fixes#9775
### What?
Enhanced Serbian translations for the lexical editor have been
implemented. The updates correct inaccuracies in the Serbian Cyrillic
translations and address various errors in the previous versions.
### Why?
- Incorrect use of Latin script in place of Cyrillic.
- Contextual errors in translations.
The join field was not respecting the defaultSort or defaultLimit of the
field configuration.
### Why?
This was never implemented.
### How?
This fix applies these correct limit and sort properties to the query,
first based on the field config and as a fallback, the collection
configuration.
Similar to #9746. When deploying to Vercel, preview deployment URLs are
dynamically generated. This breaks `admin.preview` within those
deployments because there is no mechanism by which we can detect and set
that URL within Payload. Although Vercel provides various environment
variables at our disposal, they provide no concrete identifier for
exactly which URL is being currently previewed (you can access the same
deployment from a number of different URLs).
The fix is to support relative `admin.preview` URLs, that way Payload
can prepend the application's top-level domain dynamically at
render-time in order to create a fully qualified URL. So when you visit
a Vercel preview deployment, for example, that deployment's unique URL
is used as the preview redirect, instead of the application's
root/production domain. Note: this does not fix multi-tenancy
single-domain setups, as those still require a static top-level domain
for each tenant.
### What?
Fixes issue with stale locale from searchParams
### Why?
Bad use of useEffect/useState inside our useSearchParams provider.
### How?
Memoize the locale instead of relying on the useEffect which was causing
unnecessary renders with stale values.
When deploying to Vercel, preview deployment URLs are dynamically
generated. This breaks Live Preview within those deployments because
there is no mechanism by which we can detect and set that URL within
Payload. Although Vercel provides various environment variables at our
disposal, they provide no concrete identifier for exactly _which_ URL is
being currently previewed (you an access the same deployment from a
number of different URLs).
The fix is to support _relative_ live preview URLs, that way Payload can
prepend the application's top-level domain dynamically at render-time in
order to create a fully qualified URL. So when you visit a Vercel
preview deployment, for example, that deployment's unique URL is used to
load the iframe of the preview window, instead of the application's
root/production domain. Note: this does not fix multi-tenancy
single-domain setups, as those still require a static top-level domain
for each tenant.
### What?
The `<header>` dom node was rendering even if empty for group fields.
Causing extra margin to be added even if no label/description were
provided.
### Why?
If the field had no label, description or errors it would still render.
### How?
Wraps the header node in an additional condition that checks for label,
description or errors before rendering the node.
Fixes https://github.com/payloadcms/payload/issues/9606
With Postgres / SQLite, select fields (non `hasMany: true`) weren't
properly handled in the `traverseFields.ts` function for `select` query.
### What
Updates auth.forgotPassword.expiration prop type to include JSDocs
I.e
```
/**
* The number of milliseconds that the forgot password token should be valid for.
* @default 3600000 // 1 hour
*/
```