Compare commits

..

27 Commits

Author SHA1 Message Date
Jarrod Flesch
6d5cc843a2 fix(db-mongodb): updateOne mutates the data object and does not transform it for read (#13065)
Fixes https://github.com/payloadcms/payload/issues/13045

`updateOne` when returning is `false` mutates the data object for write
operations on the DB, but that causes an issue when using that data
object later on since all of the id's are mutated to objectIDs and never
transformed back into read id's.

This fix ensures that the transform happens even when the result is not
returned.
2025-07-07 14:50:01 -04:00
Jarrod Flesch
34920a7ec0 test: fix tests that rely on remote urls (#13073) 2025-07-07 14:02:55 -04:00
Germán Jabloñski
2650eb7d44 fix(ui): increase timeout for opening list drawer in RelationshipInput (#13031)
As stated in #12529, the setTimeout was defined through trial and error
as it wasn't possible to reproduce the bug with the devtools open and
therefore with the CPU throttled. One user reported still experiencing
the bug.

I'm increasing the timeout to 100ms, which seems acceptable enough to
keep postponing a better fix, considering the bug isn't that critical.

If we find it keeps happening, we'll probably need to investigate the
root cause.
2025-07-07 09:30:09 -04:00
Jessica Rynkar
50c2f8bec2 fix(plugin-redirects): make 'from' field unique to prevent errors in redirect logic (#12964)
### What?
This PR updates the `from` field in `plugin-redirects` to add `unique:
true`.

### Why?
If you create multiple redirects with the same `from` URL — the
application won't know which one to follow, which causes errors and
unpredictable behavior.

### How?
Adds `unique: true` to the plugin injected `from` field.

### Migration Required
This change will require a migration. Projects already using this plugin
will need to:
- Ensure there are no duplicate `from` values in their existing
redirects collection.
- Remove or modify any duplicate entries before applying this update.

Fixes #12959
2025-07-07 11:21:40 +01:00
Jacob Fletcher
f49eeb1a63 fix(next): respect collection-level live preview config (#13036)
Fixes #13035.

We broke collection-level live preview configs in #12860.
2025-07-03 21:47:16 +00:00
Jarrod Flesch
1d9ad6f2f1 fix(ui): change password button is hidden when user has full field access (#12988) 2025-07-03 13:59:22 -04:00
Kendell
30fc7e3012 fix: check hostname of upload url (#13018)
Adds:
```ts
import { lookup } from 'dns/promises'
// ...
const { address } = await lookup(hostname)
// ...
return isSafeIp(address)
```

To ensure that an `ip` address is being verified. Previously, hostnames
were being verified by `isSafeIp`.


Fixes: https://github.com/payloadcms/payload/issues/12876
2025-07-03 10:50:31 -04:00
Elliot DeNolf
1ccd7ef074 chore(release): v3.45.0 [skip ci] 2025-07-03 09:23:23 -04:00
Patrik
34c3a5193b fix(plugin-import-export): pre-scan columns before streaming CSV export (#13009)
### What?

Fixes an issue where only the fields from the first batch of documents
were used to generate CSV column headers during streaming exports.

### Why?

Previously, columns were determined during the first streaming batch. If
a field appeared only in later documents, it was omitted from the CSV
entirely — leading to incomplete exports when fields were sparsely
populated across the dataset.

### How?

- Adds a **pre-scan step** before streaming begins to collect all column
keys across all pages
- Uses this superset of keys to define the final CSV header
- Ensures every row is padded to match the full column set

This matches the behavior of non-streamed exports and guarantees that
the streamed CSV output includes all relevant fields, regardless of when
they appear in pagination.
2025-07-03 08:53:02 -04:00
Sasha
81532cb9c9 fix(db-mongodb): nested sorting by ID (#13016)
Fixes sorting when the `sort` path contains a relationship and ends with
`id`, for example `sort: 'post.category.id'`.
2025-07-03 08:51:45 -04:00
Sebastian Blank
f70c6fe3e7 fix(templates): wrong link in demo content (custom components) (#13024)
### What?

The "custom component" link in the dashboard of the website demo is
wrong:

![image](https://github.com/user-attachments/assets/ee716a87-c515-4561-932d-f1c1fcccfd5e)
2025-07-03 12:07:19 +00:00
Alessio Gravili
e6b664284f chore: fix payload bundle script (#13022)
This fixes the payload bundle script. While not run by default, it's
useful for checking the payload bundle size by manually running `cd
packages/payload && node bundle.js`.
2025-07-03 04:37:44 -07:00
Alessio Gravili
fafaa04e1a fix(drizzle): ensure updateOne does not create new document if where query has no results (#12991)
Previously, `db.updateOne` calls with `where` queries that lead to no
results would create new rows on drizzle. Essentially, `db.updateOne`
behaved like `db.upsertOne` on drizzle
2025-07-02 13:56:59 -07:00
Germán Jabloñski
babcd599da fix(ui): save nested richtext inside inlineBlock (#12773)
Removing the `setTimeout` not only doesn't break any tests, but it also
fixes the linked issue.

The long comment above the if statement was added in
https://github.com/payloadcms/payload/pull/5460 and explains why the if
statement is necessary GIVEN the existence of the `setTimeout`, but the
`setTimeout` was introduced [earlier because the button apparently
didn't work](https://github.com/payloadcms/payload/issues/1414).

It seems to work now without the `setTimeout`, because otherwise the
tests wouldn't even pass. I also tested it manually, and it works fine.


Fixes #12687
2025-07-02 19:43:48 +00:00
Jessica Rynkar
ac19b78968 style(richtext-lexical): ensure error state is shown at small-break (#12827)
### What?
Shows error state (red left border) on small screens.

### Why?
The current error state disappears at small-break screen width.

### How?
Updates small-break error state to match the desktop error state for the
Lexical field.

##### Reported by client.
2025-07-02 12:16:50 -07:00
Jacob Fletcher
b40c581a27 fix(ui): autosave infinite loop within document drawer (#13007)
Required for #13005.

Opening an autosave-enabled document within a drawer triggers an
infinite loop when the root document is also autosave-enabled.

This was for two reasons:

1. Autosave would run and change the `updatedAt` timestamp. This would
trigger another run of autosave, and so on. The timestamp is now removed
before comparison to ensure that sequential autosave runs are skipped.

2. The `dequal()` call was not being given the `.current` property off
the ref object. This meant that is was never evaluate to `true` and
therefore never skip unnecessary autosaves to begin with.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210697235723932
2025-07-02 15:11:38 -04:00
Patrik
335af1b8c9 fix(plugin-import-export): preview table to include all selected columns regardless of populated data (#12985)
### What?

Ensure the export preview table includes all field keys as columns, even
if those fields are not populated in any of the returned documents.

### Why?

Previously, if none of the documents in the preview result had a value
for a given field, that column would be missing entirely from the
preview table.

### How?

- Introduced a `getFlattenedFieldKeys` utility that recursively extracts
all missing flattened field accessors from the collection’s config that
are undefined

- Updates the preview UI logic to build columns from all flattened keys,
not just the first document
2025-07-02 09:28:21 -07:00
Alessio Gravili
583a733334 feat(drizzle): support half-precision, binary, and sparse vectors column types (#12491)
Adds support for `halfvec` and `sparsevec` and `bit` (binary vector)
column types. This is required for supporting indexing of embeddings >
2000 dimensions on postgres using the pg-vector extension.
2025-07-02 19:24:53 +03:00
Jessica Rynkar
6e5ddc8873 fix(examples): only allow super admins to create users with super admin role (#13015)
### What?

This PR updates the `create` access control on the `users` collection in
the `multi-tenant` example to prevent unauthorized creation of
`super-admin` users.

### Why?

Previously, any authenticated user could create a new user and assign
them the `super-admin` role — even if they didn’t have that role
themselves. This bypassed role-based restrictions and introduced a
security vulnerability, allowing users to escalate their own privileges
by working around role restrictions during user creation.

### How?

The `create` access function now checks whether the current user has the
`super-admin` role before allowing the creation of another
`super-admin`. If not, the request is denied.


**Fixes:** `CMS2-Q225-01`
2025-07-02 15:42:55 +01:00
Jarrod Flesch
9ba740e472 fix(ui): field bulk upload showing stale data (#13006) 2025-07-02 10:11:51 -04:00
Jessica Rynkar
50029532aa fix(examples): checks requested tenant matches user tenant permissions (#13012)
### What

This PR updates the `create` access control functions in the
`multi-tenant` example to ensure that any `tenant` specified in a create
request matches a tenant the user has admin access to.

### Why

Previously, while the admin panel UI restricted the tenant selection, it
was still possible to bypass this by making a request directly to the
API with a different `tenant`. This allowed users to create documents
under tenants they shouldn't have access to.

### How

The `access` functions on the `users` and `pages` collections now
explicitly check whether the tenant(s) in the request are included in
the user's tenant permissions. If not, access is denied by returning
`false`.

**Fixes: CMS2-Q225-03**
2025-07-02 14:30:47 +01:00
Jacob Fletcher
c80b6e92c4 fix(ui): prevent document drawer from remounting on save (#13005)
Supersedes #12992. Partially closes #12975.

Right now autosave-enabled documents opened within a drawer will
unnecessarily remount on every autosave interval, causing loss of input
focus, etc. This makes it nearly impossible to edit these documents,
especially if the interval is very short.

But the same is true for non-autosave documents when "manually" saving,
e.g. pressing the "save draft" or "publish changes" buttons. This has
gone largely unnoticed, however, as the user has already lost focus of
the form to interact with these controls, and they somewhat expect this
behavior or at least accept it.

Now, the form remains mounted across autosave events and the user's
cursor never loses focus. Much better.

Before:


https://github.com/user-attachments/assets/a159cdc0-21e8-45f6-a14d-6256e53bc3df

After:


https://github.com/user-attachments/assets/cd697439-1cd3-4033-8330-a5642f7810e8

Related: #12842

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210689077645986
2025-07-02 09:07:08 -04:00
Jarrod Flesch
a9580e05ac fix: disable graphql introspection queries when disableIntrospectionInProduction is true (#12982) 2025-07-02 08:33:20 -04:00
Jarrod Flesch
57d00ad2e9 test: reduce queue test amount (#13008) 2025-07-01 15:55:16 -04:00
Jarrod Flesch
a9ad7c771e fix(ui): bulk upload redirecting to relationship documents when added (#13001)
Fixes https://github.com/payloadcms/payload/issues/12786
2025-07-01 15:23:11 -04:00
Patrik
7a40a9fc06 fix(ui): skip disabled fields when adding OR filter conditions in list view (#13004)
### What?

Fixes a bug where adding an additional OR filter condition in the list
view selects a field with `admin.disableListFilter: true`, causing all
filter fields to appear disabled.

### Why?

When the first field in a collection has `disableListFilter` set to
`true`, adding a second OR condition defaults to using that field. This
leads to a broken filter UI where no valid fields are selectable.

### How?

Replaces the hardcoded usage of `reducedFields[0]` with a call to
`reducedFields.find(...) `that skips fields with `disableListFilter:
true`, consistent with the logic already used when adding the first
filter condition.

Fixes #12993
2025-07-01 11:35:48 -07:00
Patrik
b1ae749311 fix(ui): render preview sizes button when adjustments are disabled but image sizes are defined (#12999)
### What?

The "Preview Sizes" button in the file upload UI was not showing up if:
- `crop` and `focalPoint` were both `false`
- No `customUploadActions` were provided
- But image sizes were configured

### Why?

This happened because `UploadActions` wasn’t rendered at all unless
adjustments or custom actions were present.

### How?

Update the conditional in `StaticFileDetails` to also render
`UploadActions` when:
- `hasImageSizes` is `true` and the document has a `filename`

Fixes #12832
2025-07-01 07:44:48 -07:00
130 changed files with 2125 additions and 917 deletions

View File

@@ -187,7 +187,8 @@ jobs:
services:
postgres:
image: ${{ (startsWith(matrix.database, 'postgres') ) && 'postgis/postgis:16-3.4' || '' }}
# Custom postgres 17 docker image that supports both pg-vector and postgis: https://github.com/payloadcms/postgis-vector
image: ${{ (startsWith(matrix.database, 'postgres') ) && 'ghcr.io/payloadcms/postgis-vector:latest' || '' }}
env:
# must specify password for PG Docker container image, see: https://registry.hub.docker.com/_/postgres?tab=description&page=1&name=10
POSTGRES_USER: ${{ env.POSTGRES_USER }}

View File

@@ -16,14 +16,15 @@ The labels you provide for your Collections and Globals are used to name the Gra
At the top of your Payload Config you can define all the options to manage GraphQL.
| Option | Description |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `mutations` | Any custom Mutations to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `queries` | Any custom Queries to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/graphql/overview#query-complexity-limits) |
| `disablePlaygroundInProduction` | A boolean that if false will enable the GraphQL playground, defaults to true. [More](/docs/graphql/overview#graphql-playground) |
| `disable` | A boolean that if true will disable the GraphQL entirely, defaults to false. |
| `validationRules` | A function that takes the ExecutionArgs and returns an array of ValidationRules. |
| Option | Description |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mutations` | Any custom Mutations to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `queries` | Any custom Queries to be added in addition to what Payload provides. [More](/docs/graphql/extending) |
| `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/graphql/overview#query-complexity-limits) |
| `disablePlaygroundInProduction` | A boolean that if false will enable the GraphQL playground in production environments, defaults to true. [More](/docs/graphql/overview#graphql-playground) |
| `disableIntrospectionInProduction` | A boolean that if false will enable the GraphQL introspection in production environments, defaults to true. |
| `disable` | A boolean that if true will disable the GraphQL entirely, defaults to false. |
| `validationRules` | A function that takes the ExecutionArgs and returns an array of ValidationRules. |
## Collections

View File

@@ -53,7 +53,7 @@ export default buildConfig({
admin: {
components: {
// The `BeforeLogin` component renders a message that you see while logging into your admin panel.
// Feel free to delete this at any time. Simply remove the line below and the import `BeforeLogin` statement on line 15.
// Feel free to delete this at any time. Simply remove the line below.
beforeLogin: ['@/components/BeforeLogin'],
afterDashboard: ['@/components/AfterDashboard'],
},

View File

@@ -14,9 +14,12 @@ export const superAdminOrTenantAdminAccess: Access = ({ req }) => {
return true
}
return {
tenant: {
in: getUserTenantIDs(req.user, 'tenant-admin'),
},
const adminTenantAccessIDs = getUserTenantIDs(req.user, 'tenant-admin')
const requestedTenant = req?.data?.tenant
if (requestedTenant && adminTenantAccessIDs.includes(requestedTenant)) {
return true
}
return false
}

View File

@@ -1,6 +1,6 @@
import type { Access } from 'payload'
import type { User } from '../../../payload-types'
import type { Tenant, User } from '../../../payload-types'
import { isSuperAdmin } from '../../../access/isSuperAdmin'
import { getUserTenantIDs } from '../../../utilities/getUserTenantIDs'
@@ -14,9 +14,20 @@ export const createAccess: Access<User> = ({ req }) => {
return true
}
if (!isSuperAdmin(req.user) && req.data?.roles?.includes('super-admin')) {
return false
}
const adminTenantAccessIDs = getUserTenantIDs(req.user, 'tenant-admin')
if (adminTenantAccessIDs.length) {
const requestedTenants: Tenant['id'][] =
req.data?.tenants?.map((t: { tenant: Tenant['id'] }) => t.tenant) ?? []
const hasAccessToAllRequestedTenants = requestedTenants.every((tenantID) =>
adminTenantAccessIDs.includes(tenantID),
)
if (hasAccessToAllRequestedTenants) {
return true
}

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.44.0",
"version": "3.45.0",
"private": true,
"type": "module",
"workspaces": [
@@ -151,8 +151,8 @@
"create-payload-app": "workspace:*",
"cross-env": "7.0.3",
"dotenv": "16.4.7",
"drizzle-kit": "0.31.0",
"drizzle-orm": "0.43.1",
"drizzle-kit": "0.31.4",
"drizzle-orm": "0.44.2",
"escape-html": "^1.0.3",
"execa": "5.1.1",
"form-data": "3.0.1",
@@ -166,7 +166,7 @@
"next": "15.3.2",
"open": "^10.1.0",
"p-limit": "^5.0.0",
"pg": "8.11.3",
"pg": "8.16.3",
"playwright": "1.50.0",
"playwright-core": "1.50.0",
"prettier": "3.5.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/admin-bar",
"version": "3.44.0",
"version": "3.45.0",
"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.44.0",
"version": "3.45.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

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

View File

@@ -77,6 +77,9 @@ const relationshipSort = ({
) {
const relationshipPath = segments.slice(0, i + 1).join('.')
let sortFieldPath = segments.slice(i + 1, segments.length).join('.')
if (sortFieldPath.endsWith('.id')) {
sortFieldPath = sortFieldPath.split('.').slice(0, -1).join('.')
}
if (Array.isArray(field.relationTo)) {
throw new APIError('Not supported')
}

View File

@@ -55,6 +55,7 @@ export const updateOne: UpdateOne = async function updateOne(
try {
if (returning === false) {
await Model.updateOne(query, data, options)
transform({ adapter: this, data, fields, operation: 'read' })
return null
} else {
result = await Model.findOneAndUpdate(query, data, options)

View File

@@ -417,7 +417,7 @@ export const transform = ({
if (operation === 'read') {
delete data['__v']
data.id = data._id
data.id = data._id || data.id
delete data['_id']
if (data.id instanceof Types.ObjectId) {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.44.0",
"version": "3.45.0",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -78,9 +78,9 @@
"@payloadcms/drizzle": "workspace:*",
"@types/pg": "8.10.2",
"console-table-printer": "2.12.1",
"drizzle-kit": "0.31.1",
"drizzle-kit": "0.31.4",
"drizzle-orm": "0.44.2",
"pg": "8.11.3",
"pg": "8.16.3",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",
"uuid": "10.0.0"

View File

@@ -37,6 +37,7 @@ import {
updateMany,
updateOne,
updateVersion,
upsert,
} from '@payloadcms/drizzle'
import {
columnToCodeConverter,
@@ -207,7 +208,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
updateMany,
updateOne,
updateVersion,
upsert: updateOne,
upsert,
})
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.44.0",
"version": "3.45.0",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -76,7 +76,7 @@
"@libsql/client": "0.14.0",
"@payloadcms/drizzle": "workspace:*",
"console-table-printer": "2.12.1",
"drizzle-kit": "0.31.1",
"drizzle-kit": "0.31.4",
"drizzle-orm": "0.44.2",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",

View File

@@ -38,6 +38,7 @@ import {
updateMany,
updateOne,
updateVersion,
upsert,
} from '@payloadcms/drizzle'
import { like, notLike } from 'drizzle-orm'
import { createDatabaseAdapter, defaultBeginTransaction } from 'payload'
@@ -189,7 +190,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
updateGlobalVersion,
updateOne,
updateVersion,
upsert: updateOne,
upsert,
})
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
"version": "3.44.0",
"version": "3.45.0",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -78,9 +78,9 @@
"@payloadcms/drizzle": "workspace:*",
"@vercel/postgres": "^0.9.0",
"console-table-printer": "2.12.1",
"drizzle-kit": "0.31.1",
"drizzle-kit": "0.31.4",
"drizzle-orm": "0.44.2",
"pg": "8.11.3",
"pg": "8.16.3",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",
"uuid": "10.0.0"

View File

@@ -38,6 +38,7 @@ import {
updateMany,
updateOne,
updateVersion,
upsert,
} from '@payloadcms/drizzle'
import {
columnToCodeConverter,
@@ -202,7 +203,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
updateMany,
updateOne,
updateVersion,
upsert: updateOne,
upsert,
})
}

View File

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

View File

@@ -78,6 +78,7 @@ export { updateJobs } from './updateJobs.js'
export { updateMany } from './updateMany.js'
export { updateOne } from './updateOne.js'
export { updateVersion } from './updateVersion.js'
export { upsert } from './upsert.js'
export { upsertRow } from './upsertRow/index.js'
export { buildCreateMigration } from './utilities/buildCreateMigration.js'
export { buildIndexName } from './utilities/buildIndexName.js'

View File

@@ -24,20 +24,26 @@ export const columnToCodeConverter: ColumnToCodeConverter = ({
const columnBuilderArgsArray: string[] = []
if (column.type === 'timestamp') {
columnBuilderArgsArray.push(`mode: '${column.mode}'`)
if (column.withTimezone) {
columnBuilderArgsArray.push('withTimezone: true')
switch (column.type) {
case 'bit':
case 'halfvec':
case 'sparsevec':
case 'vector': {
if (column.dimensions) {
columnBuilderArgsArray.push(`dimensions: ${column.dimensions}`)
}
break
}
case 'timestamp': {
columnBuilderArgsArray.push(`mode: '${column.mode}'`)
if (column.withTimezone) {
columnBuilderArgsArray.push('withTimezone: true')
}
if (typeof column.precision === 'number') {
columnBuilderArgsArray.push(`precision: ${column.precision}`)
}
}
if (column.type === 'vector') {
if (column.dimensions) {
columnBuilderArgsArray.push(`dimensions: ${column.dimensions}`)
if (typeof column.precision === 'number') {
columnBuilderArgsArray.push(`precision: ${column.precision}`)
}
break
}
}

View File

@@ -1,13 +1,16 @@
import type { ForeignKeyBuilder, IndexBuilder } from 'drizzle-orm/pg-core'
import {
bit,
boolean,
foreignKey,
halfvec,
index,
integer,
jsonb,
numeric,
serial,
sparsevec,
text,
timestamp,
uniqueIndex,
@@ -44,6 +47,14 @@ export const buildDrizzleTable = ({
for (const [key, column] of Object.entries(rawTable.columns)) {
switch (column.type) {
case 'bit': {
const builder = bit(column.name, { dimensions: column.dimensions })
columns[key] = builder
break
}
case 'enum':
if ('locale' in column) {
columns[key] = adapter.enums.enum__locales(column.name)
@@ -56,6 +67,21 @@ export const buildDrizzleTable = ({
}
break
case 'halfvec': {
const builder = halfvec(column.name, { dimensions: column.dimensions })
columns[key] = builder
break
}
case 'sparsevec': {
const builder = sparsevec(column.name, { dimensions: column.dimensions })
columns[key] = builder
break
}
case 'timestamp': {
let builder = timestamp(column.name, {
mode: column.mode,

View File

@@ -281,12 +281,30 @@ export type VectorRawColumn = {
type: 'vector'
} & BaseRawColumn
export type HalfVecRawColumn = {
dimensions?: number
type: 'halfvec'
} & BaseRawColumn
export type SparseVecRawColumn = {
dimensions?: number
type: 'sparsevec'
} & BaseRawColumn
export type BinaryVecRawColumn = {
dimensions?: number
type: 'bit'
} & BaseRawColumn
export type RawColumn =
| ({
type: 'boolean' | 'geometry' | 'jsonb' | 'numeric' | 'serial' | 'text' | 'varchar'
} & BaseRawColumn)
| BinaryVecRawColumn
| EnumRawColumn
| HalfVecRawColumn
| IntegerRawColumn
| SparseVecRawColumn
| TimestampRawColumn
| UUIDRawColumn
| VectorRawColumn

View File

@@ -18,6 +18,7 @@ export const updateOne: UpdateOne = async function updateOne(
data,
joins: joinQuery,
locale,
options = { upsert: false },
req,
returning,
select,
@@ -66,6 +67,13 @@ export const updateOne: UpdateOne = async function updateOne(
}
}
if (!idToUpdate && !options.upsert) {
// TODO: In 4.0, if returning === false, we should differentiate between:
// - No document found to update
// - Document found, but returning === false
return null
}
const result = await upsertRow({
id: idToUpdate,
adapter: this,

View File

@@ -0,0 +1,20 @@
import type { Upsert } from 'payload'
import type { DrizzleAdapter } from './types.js'
export const upsert: Upsert = async function upsert(
this: DrizzleAdapter,
{ collection, data, joins, locale, req, returning, select, where },
) {
return this.updateOne({
collection,
data,
joins,
locale,
options: { upsert: true },
req,
returning,
select,
where,
})
}

View File

@@ -13,6 +13,13 @@ import { deleteExistingArrayRows } from './deleteExistingArrayRows.js'
import { deleteExistingRowsByPath } from './deleteExistingRowsByPath.js'
import { insertArrays } from './insertArrays.js'
/**
* If `id` is provided, it will update the row with that ID.
* If `where` is provided, it will update the row that matches the `where`
* If neither `id` nor `where` is provided, it will create a new row.
*
* This function replaces the entire row and does not support partial updates.
*/
export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>({
id,
adapter,

View File

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

View File

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

View File

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

View File

@@ -113,6 +113,7 @@ export function configToSchema(config: SanitizedConfig): {
variables: args.variableValues,
// onComplete: (complexity) => { console.log('Query Complexity:', complexity); },
}),
...(config.graphQL.disableIntrospectionInProduction ? [NoProductionIntrospection] : []),
...(typeof config?.graphQL?.validationRules === 'function'
? config.graphQL.validationRules(args)
: []),
@@ -123,3 +124,18 @@ export function configToSchema(config: SanitizedConfig): {
validationRules,
}
}
const NoProductionIntrospection: GraphQL.ValidationRule = (context) => ({
Field(node) {
if (process.env.NODE_ENV === 'production') {
if (node.name.value === '__schema' || node.name.value === '__type') {
context.reportError(
new GraphQL.GraphQLError(
'GraphQL introspection is not allowed, but the query contained __schema or __type',
{ nodes: [node] },
),
)
}
}
},
})

View File

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

View File

@@ -146,7 +146,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
return (
<EntityVisibilityProvider visibleEntities={visibleEntities}>
<BulkUploadProvider>
<BulkUploadProvider drawerSlugPrefix={collectionSlug}>
<ActionsProvider Actions={Actions}>
{RenderServerComponent({
clientProps,

View File

@@ -6,6 +6,7 @@ import type {
DocumentViewServerProps,
DocumentViewServerPropsOnly,
EditViewComponent,
LivePreviewConfig,
PayloadComponent,
RenderDocumentVersionsProperties,
} from 'payload'
@@ -91,7 +92,6 @@ export const renderDocument = async ({
payload: {
config,
config: {
admin: { livePreview: livePreviewConfig },
routes: { admin: adminRoute, api: apiRoute },
serverURL,
},
@@ -329,6 +329,12 @@ export const renderDocument = async ({
viewType,
}
const livePreviewConfig: LivePreviewConfig = {
...(config.admin.livePreview || {}),
...(collectionConfig?.admin?.livePreview || {}),
...(globalConfig?.admin?.livePreview || {}),
}
const livePreviewURL =
typeof livePreviewConfig?.url === 'function'
? await livePreviewConfig.url({

View File

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

View File

@@ -7,11 +7,11 @@ const dirname = path.dirname(filename)
async function build() {
const resultIndex = await esbuild.build({
entryPoints: ['src/exports/index.ts'],
entryPoints: ['src/index.ts'],
bundle: true,
platform: 'node',
format: 'esm',
outfile: 'dist/exports/index.js',
outfile: 'dist/index.js',
splitting: false,
external: [
'lodash',

View File

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

View File

@@ -123,6 +123,7 @@ export const addDefaultsToConfig = (config: Config): Config => {
config.endpoints = config.endpoints ?? []
config.globals = config.globals ?? []
config.graphQL = {
disableIntrospectionInProduction: true,
disablePlaygroundInProduction: true,
maxComplexity: 1000,
schemaOutputFile: `${typeof process?.cwd === 'function' ? process.cwd() : ''}/schema.graphql`,

View File

@@ -1029,6 +1029,17 @@ export type Config = {
*/
graphQL?: {
disable?: boolean
/**
* Disable introspection queries in production.
*
* @default true
*/
disableIntrospectionInProduction?: boolean
/**
* Disable the GraphQL Playground in production.
*
* @default true
*/
disablePlaygroundInProduction?: boolean
maxComplexity?: number
/**

View File

@@ -1,5 +1,6 @@
import type { Dispatcher } from 'undici'
import { lookup } from 'dns/promises'
import ipaddr from 'ipaddr.js'
import { Agent, fetch as undiciFetch } from 'undici'
@@ -24,12 +25,26 @@ const isSafeIp = (ip: string) => {
return true
}
/**
* Checks if a hostname or IP address is safe to fetch from.
* @param hostname a hostname or IP address
* @returns
*/
const isSafe = async (hostname: string) => {
try {
if (ipaddr.isValid(hostname)) {
return isSafeIp(hostname)
}
const { address } = await lookup(hostname)
return isSafeIp(address)
} catch (_ignore) {
return false
}
}
const ssrfFilterInterceptor: Dispatcher.DispatcherComposeInterceptor = (dispatch) => {
return (opts, handler) => {
const url = new URL(opts.origin?.toString() + opts.path)
if (!isSafeIp(url.hostname)) {
throw new Error(`Blocked unsafe attempt to ${url}`)
}
return dispatch(opts, handler)
}
}
@@ -40,11 +55,20 @@ const safeDispatcher = new Agent().compose(ssrfFilterInterceptor)
* A "safe" version of undici's fetch that prevents SSRF attacks.
*
* - Utilizes a custom dispatcher that filters out requests to unsafe IP addresses.
* - Validates domain names by resolving them to IP addresses and checking if they're safe.
* - Undici was used because it supported interceptors as well as "credentials: include". Native fetch
*/
export const safeFetch = async (...args: Parameters<typeof undiciFetch>) => {
const [url, options] = args
const [unverifiedUrl, options] = args
try {
const url = new URL(unverifiedUrl)
const isHostnameSafe = await isSafe(url.hostname)
if (!isHostnameSafe) {
throw new Error(`Blocked unsafe attempt to ${url.toString()}`)
}
return await undiciFetch(url, {
...options,
dispatcher: safeDispatcher,
@@ -56,11 +80,13 @@ export const safeFetch = async (...args: Parameters<typeof undiciFetch>) => {
// The desired message we want to bubble up is in the cause
throw new Error(error.cause.message)
} else {
let stringifiedUrl: string | undefined | URL = undefined
if (typeof url === 'string' || url instanceof URL) {
stringifiedUrl = url
} else if (url instanceof Request) {
stringifiedUrl = url.url
let stringifiedUrl: string | undefined = undefined
if (typeof unverifiedUrl === 'string') {
stringifiedUrl = unverifiedUrl
} else if (unverifiedUrl instanceof URL) {
stringifiedUrl = unverifiedUrl.toString()
} else if (unverifiedUrl instanceof Request) {
stringifiedUrl = unverifiedUrl.url
}
throw new Error(`Failed to fetch from ${stringifiedUrl}, ${error.message}`)

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
"version": "3.44.0",
"version": "3.45.0",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-import-export",
"version": "3.44.0",
"version": "3.45.0",
"description": "Import-Export plugin for Payload",
"keywords": [
"payload",

View File

@@ -74,11 +74,12 @@ export const Preview = () => {
return
}
const { docs, totalDocs } = await res.json()
const { docs, totalDocs }: { docs: Record<string, unknown>[]; totalDocs: number } =
await res.json()
setResultCount(limit && limit < totalDocs ? limit : totalDocs)
const allKeys = Object.keys(docs[0] || {})
const allKeys = Array.from(new Set(docs.flatMap((doc) => Object.keys(doc))))
const defaultMetaFields = ['createdAt', 'updatedAt', '_status', 'id']
// Match CSV column ordering by building keys based on fields and regex
@@ -96,13 +97,10 @@ export const Preview = () => {
})
: allKeys.filter((key) => !defaultMetaFields.includes(key))
const includedMeta = new Set(selectedKeys)
const missingMetaFields = defaultMetaFields.flatMap((field) => {
const regex = fieldToRegex(field)
return allKeys.filter((key) => regex.test(key) && !includedMeta.has(key))
})
const fieldKeys = [...selectedKeys, ...missingMetaFields]
const fieldKeys =
Array.isArray(fields) && fields.length > 0
? selectedKeys // strictly only what was selected
: [...selectedKeys, ...defaultMetaFields.filter((key) => allKeys.includes(key))]
// Build columns based on flattened keys
const newColumns: Column[] = fieldKeys.map((key) => ({

View File

@@ -111,23 +111,45 @@ export const createExport = async (args: CreateExportArgs) => {
if (download) {
if (debug) {
req.payload.logger.info('Starting download stream')
req.payload.logger.info('Pre-scanning all columns before streaming')
}
const allColumnsSet = new Set<string>()
const allColumns: string[] = []
let scanPage = 1
let hasMore = true
while (hasMore) {
const result = await payload.find({ ...findArgs, page: scanPage })
result.docs.forEach((doc) => {
const flat = flattenObject({ doc, fields, toCSVFunctions })
Object.keys(flat).forEach((key) => {
if (!allColumnsSet.has(key)) {
allColumnsSet.add(key)
allColumns.push(key)
}
})
})
hasMore = result.hasNextPage
scanPage += 1
}
if (debug) {
req.payload.logger.info(`Discovered ${allColumns.length} columns`)
}
const encoder = new TextEncoder()
let isFirstBatch = true
let columns: string[] | undefined
let page = 1
let streamPage = 1
const stream = new Readable({
async read() {
const result = await payload.find({
...findArgs,
page,
})
const result = await payload.find({ ...findArgs, page: streamPage })
if (debug) {
req.payload.logger.info(`Processing batch ${page} with ${result.docs.length} documents`)
req.payload.logger.info(`Streaming batch ${streamPage} with ${result.docs.length} docs`)
}
if (result.docs.length === 0) {
@@ -135,19 +157,24 @@ export const createExport = async (args: CreateExportArgs) => {
return
}
const csvInput = result.docs.map((doc) => flattenObject({ doc, fields, toCSVFunctions }))
const batchRows = result.docs.map((doc) => flattenObject({ doc, fields, toCSVFunctions }))
if (isFirstBatch) {
columns = Object.keys(csvInput[0] ?? {})
}
const paddedRows = batchRows.map((row) => {
const fullRow: Record<string, unknown> = {}
for (const col of allColumns) {
fullRow[col] = row[col] ?? ''
}
return fullRow
})
const csvString = stringify(csvInput, {
const csvString = stringify(paddedRows, {
header: isFirstBatch,
columns,
columns: allColumns,
})
this.push(encoder.encode(csvString))
isFirstBatch = false
streamPage += 1
if (!result.hasNextPage) {
if (debug) {
@@ -155,8 +182,6 @@ export const createExport = async (args: CreateExportArgs) => {
}
this.push(null) // End the stream
}
page += 1
},
})
@@ -168,11 +193,15 @@ export const createExport = async (args: CreateExportArgs) => {
})
}
// Non-download path (buffered export)
if (debug) {
req.payload.logger.info('Starting file generation')
}
const outputData: string[] = []
let isFirstBatch = true
const rows: Record<string, unknown>[] = []
const columnsSet = new Set<string>()
const columns: string[] = []
let page = 1
let hasNextPage = true
@@ -189,9 +218,19 @@ export const createExport = async (args: CreateExportArgs) => {
}
if (isCSV) {
const csvInput = result.docs.map((doc) => flattenObject({ doc, fields, toCSVFunctions }))
outputData.push(stringify(csvInput, { header: isFirstBatch }))
isFirstBatch = false
const batchRows = result.docs.map((doc) => flattenObject({ doc, fields, toCSVFunctions }))
// Track discovered column keys
batchRows.forEach((row) => {
Object.keys(row).forEach((key) => {
if (!columnsSet.has(key)) {
columnsSet.add(key)
columns.push(key)
}
})
})
rows.push(...batchRows)
} else {
const jsonInput = result.docs.map((doc) => JSON.stringify(doc))
outputData.push(jsonInput.join(',\n'))
@@ -201,6 +240,23 @@ export const createExport = async (args: CreateExportArgs) => {
page += 1
}
if (isCSV) {
const paddedRows = rows.map((row) => {
const fullRow: Record<string, unknown> = {}
for (const col of columns) {
fullRow[col] = row[col] ?? ''
}
return fullRow
})
outputData.push(
stringify(paddedRows, {
header: true,
columns,
}),
)
}
const buffer = Buffer.from(format === 'json' ? `[${outputData.join(',')}]` : outputData.join(''))
if (debug) {
req.payload.logger.info(`${format} file generation complete`)

View File

@@ -11,6 +11,7 @@ import { getCustomFieldFunctions } from './export/getCustomFieldFunctions.js'
import { getSelect } from './export/getSelect.js'
import { getExportCollection } from './getExportCollection.js'
import { translations } from './translations/index.js'
import { getFlattenedFieldKeys } from './utilities/getFlattenedFieldKeys.js'
export const importExportPlugin =
(pluginConfig: ImportExportPluginConfig) =>
@@ -112,13 +113,23 @@ export const importExportPlugin =
select,
})
const transformed = docs.map((doc) =>
flattenObject({
const possibleKeys = getFlattenedFieldKeys(collection.config.fields as FlattenedField[])
const transformed = docs.map((doc) => {
const row = flattenObject({
doc,
fields,
toCSVFunctions,
}),
)
})
for (const key of possibleKeys) {
if (!(key in row)) {
row[key] = null
}
}
return row
})
return Response.json({
docs: transformed,

View File

@@ -0,0 +1,75 @@
import { type FlattenedField } from 'payload'
type FieldWithPresentational =
| {
fields?: FlattenedField[]
name?: string
tabs?: {
fields: FlattenedField[]
name?: string
}[]
type: 'collapsible' | 'row' | 'tabs'
}
| FlattenedField
export const getFlattenedFieldKeys = (fields: FieldWithPresentational[], prefix = ''): string[] => {
const keys: string[] = []
fields.forEach((field) => {
if (!('name' in field) || typeof field.name !== 'string') {
return
}
const name = prefix ? `${prefix}_${field.name}` : field.name
switch (field.type) {
case 'array': {
const subKeys = getFlattenedFieldKeys(field.fields as FlattenedField[], `${name}_0`)
keys.push(...subKeys)
break
}
case 'blocks':
field.blocks.forEach((block) => {
const blockKeys = getFlattenedFieldKeys(block.fields as FlattenedField[], `${name}_0`)
keys.push(...blockKeys)
})
break
case 'collapsible':
case 'group':
case 'row':
keys.push(...getFlattenedFieldKeys(field.fields as FlattenedField[], name))
break
case 'relationship':
if (field.hasMany) {
// e.g. hasManyPolymorphic_0_value_id
keys.push(`${name}_0_relationTo`, `${name}_0_value_id`)
} else {
// e.g. hasOnePolymorphic_id
keys.push(`${name}_id`, `${name}_relationTo`)
}
break
case 'tabs':
if (field.tabs) {
field.tabs.forEach((tab) => {
if (tab.name) {
const tabPrefix = prefix ? `${prefix}_${tab.name}` : tab.name
keys.push(...getFlattenedFieldKeys(tab.fields, tabPrefix))
} else {
keys.push(...getFlattenedFieldKeys(tab.fields, prefix))
}
})
}
break
default:
if ('hasMany' in field && field.hasMany) {
// Push placeholder for first index
keys.push(`${name}_0`)
} else {
keys.push(name)
}
break
}
})
return keys
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-multi-tenant",
"version": "3.44.0",
"version": "3.45.0",
"description": "Multi Tenant plugin for Payload",
"keywords": [
"payload",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-redirects",
"version": "3.44.0",
"version": "3.45.0",
"description": "Redirects plugin for Payload",
"keywords": [
"payload",

View File

@@ -28,6 +28,7 @@ export const redirectsPlugin =
index: true,
label: 'From URL',
required: true,
unique: true,
},
{
name: 'to',

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-search",
"version": "3.44.0",
"version": "3.45.0",
"description": "Search plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-sentry",
"version": "3.44.0",
"version": "3.45.0",
"description": "Sentry plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-seo",
"version": "3.44.0",
"version": "3.45.0",
"description": "SEO plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-stripe",
"version": "3.44.0",
"version": "3.45.0",
"description": "Stripe plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "3.44.0",
"version": "3.45.0",
"description": "The officially supported Lexical richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -67,6 +67,16 @@ $lexical-contenteditable-bottom-padding: 8px;
}
}
}
@include small-break {
.rich-text-lexical {
&.error {
> .rich-text-lexical__wrap {
@include lightInputError;
}
}
}
}
}
html[data-theme='dark'] {
@@ -81,5 +91,15 @@ $lexical-contenteditable-bottom-padding: 8px;
}
}
}
@include small-break {
.rich-text-lexical {
&.error {
> .rich-text-lexical__wrap {
@include darkInputError;
}
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-slate",
"version": "3.44.0",
"version": "3.45.0",
"description": "The officially supported Slate richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-azure",
"version": "3.44.0",
"version": "3.45.0",
"description": "Payload storage adapter for Azure Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-gcs",
"version": "3.44.0",
"version": "3.45.0",
"description": "Payload storage adapter for Google Cloud Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-s3",
"version": "3.44.0",
"version": "3.45.0",
"description": "Payload storage adapter for Amazon S3",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-uploadthing",
"version": "3.44.0",
"version": "3.45.0",
"description": "Payload storage adapter for uploadthing",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-vercel-blob",
"version": "3.44.0",
"version": "3.45.0",
"description": "Payload storage adapter for Vercel Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/translations",
"version": "3.44.0",
"version": "3.45.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/ui",
"version": "3.44.0",
"version": "3.45.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -52,7 +52,13 @@ export const AddNewRelation: React.FC<Props> = ({
const onSave: DocumentDrawerContextType['onSave'] = useCallback(
({ doc, operation }) => {
if (operation === 'create') {
// if autosave is enabled, the operation will be 'update'
const isAutosaveEnabled =
typeof collectionConfig?.versions?.drafts === 'object'
? collectionConfig.versions.drafts.autosave
: false
if (operation === 'create' || (operation === 'update' && isAutosaveEnabled)) {
// ensure the value is not already in the array
let isNewValue = false
if (!value) {

View File

@@ -23,6 +23,7 @@ import { useLocale } from '../../providers/Locale/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { formatTimeToNow } from '../../utilities/formatDocTitle/formatDateTitle.js'
import { reduceFieldsToValuesWithValidation } from '../../utilities/reduceFieldsToValuesWithValidation.js'
import { useDocumentDrawerContext } from '../DocumentDrawer/Provider.js'
import { LeaveWithoutSaving } from '../LeaveWithoutSaving/index.js'
import './index.scss'
@@ -56,10 +57,12 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
updateSavedDocumentData,
} = useDocumentInfo()
const { reportUpdate } = useDocumentEvents()
const { dispatchFields, isValid, setBackgroundProcessing, setIsValid, setSubmitted } = useForm()
const { onSave: onSaveFromDocumentDrawer } = useDocumentDrawerContext()
const [fields] = useAllFormFields()
const { reportUpdate } = useDocumentEvents()
const { dispatchFields, isValid, setBackgroundProcessing, setIsValid } = useForm()
const [formState] = useAllFormFields()
const modified = useFormModified()
const submitted = useFormSubmitted()
@@ -78,32 +81,27 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
)
const [_saving, setSaving] = useState(false)
const saving = useDeferredValue(_saving)
const debouncedFields = useDebounce(fields, interval)
const fieldRef = useRef(fields)
const debouncedFormState = useDebounce(formState, interval)
const formStateRef = useRef(formState)
const modifiedRef = useRef(modified)
const localeRef = useRef(locale)
/**
* Track the validation internally so Autosave can determine when to run queue processing again
* Helps us prevent infinite loops when the queue is processing and the form is invalid
*/
const isValidRef = useRef(isValid)
// Store fields in ref so the autosave func
// can always retrieve the most to date copies
// after the timeout has executed
fieldRef.current = fields
formStateRef.current = formState
// Store modified in ref so the autosave func
// can bail out if modified becomes false while
// timing out during autosave
modifiedRef.current = modified
// Store locale in ref so the autosave func
// can always retrieve the most to date locale
localeRef.current = locale
const { queueTask } = useQueues()
@@ -155,14 +153,14 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
if (url) {
if (modifiedRef.current) {
const { data, valid } = reduceFieldsToValuesWithValidation(fieldRef.current, true)
const { data, valid } = reduceFieldsToValuesWithValidation(formStateRef.current, true)
data._status = 'draft'
const skipSubmission =
submitted && !valid && versionsConfig?.drafts && versionsConfig?.drafts?.validate
if (!skipSubmission && isValidRef.current) {
if (!skipSubmission) {
let res
try {
@@ -183,6 +181,8 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
// We need to log the time in order to figure out if we need to trigger the state off later
endTimestamp = newDate.getTime()
const json = await res.json()
if (res.status === 200) {
setLastUpdateTime(newDate.getTime())
@@ -192,13 +192,20 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
updatedAt: newDate.toISOString(),
})
// if onSaveFromDocumentDrawer is defined, call it
if (typeof onSaveFromDocumentDrawer === 'function') {
void onSaveFromDocumentDrawer({
...json,
operation: 'update',
})
}
if (!mostRecentVersionIsAutosaved) {
incrementVersionCount()
setMostRecentVersionIsAutosaved(true)
setUnpublishedVersionCount((prev) => prev + 1)
}
}
const json = await res.json()
if (versionsConfig?.drafts && versionsConfig?.drafts?.validate && json?.errors) {
if (Array.isArray(json.errors)) {
@@ -238,10 +245,7 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
toast.error(err.message || i18n.t('error:unknown'))
})
// Set valid to false internally so the queue doesn't process
isValidRef.current = false
setIsValid(false)
setSubmitted(true)
hideIndicator()
return
}
@@ -252,9 +256,6 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
// Manually update the data since this function doesn't fire the `submit` function from useForm
if (document) {
setIsValid(true)
// Reset internal state allowing the queue to process
isValidRef.current = true
updateSavedDocumentData(document)
}
}
@@ -270,11 +271,6 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
setBackgroundProcessing(false)
},
beforeProcess: () => {
if (!isValidRef.current) {
isValidRef.current = true
return false
}
setBackgroundProcessing(true)
},
},
@@ -282,7 +278,8 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
})
const didMount = useRef(false)
const previousDebouncedFieldValues = useRef(reduceFieldsToValues(debouncedFields))
const previousDebouncedData = useRef(reduceFieldsToValues(debouncedFormState))
// When debounced fields change, autosave
useEffect(() => {
/**
@@ -295,16 +292,19 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
/**
* Ensure autosave only runs if the form data changes, not every time the entire form state changes
* Remove `updatedAt` from comparison as it changes on every autosave interval.
*/
const debouncedFieldValues = reduceFieldsToValues(debouncedFields)
if (dequal(debouncedFieldValues, previousDebouncedFieldValues)) {
const { updatedAt: _, ...formData } = reduceFieldsToValues(debouncedFormState)
const { updatedAt: __, ...prevFormData } = previousDebouncedData.current
if (dequal(formData, prevFormData)) {
return
}
previousDebouncedFieldValues.current = debouncedFieldValues
previousDebouncedData.current = formData
handleAutosave()
}, [debouncedFields])
}, [debouncedFormState])
/**
* If component unmounts, clear the autosave timeout

View File

@@ -1,7 +1,5 @@
'use client'
import { useRouter, useSearchParams } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React, { useCallback, useEffect } from 'react'
import type { EditFormProps } from './types.js'
@@ -12,9 +10,7 @@ import { WatchChildErrors } from '../../../forms/WatchChildErrors/index.js'
import { useConfig } from '../../../providers/Config/index.js'
import { useDocumentEvents } from '../../../providers/DocumentEvents/index.js'
import { useDocumentInfo } from '../../../providers/DocumentInfo/index.js'
import { useEditDepth } from '../../../providers/EditDepth/index.js'
import { OperationProvider } from '../../../providers/Operation/index.js'
import { useRouteTransition } from '../../../providers/RouteTransition/index.js'
import { useServerFunctions } from '../../../providers/ServerFunctions/index.js'
import { abortAndIgnore, handleAbortRef } from '../../../utilities/abortAndIgnore.js'
import { useDocumentDrawerContext } from '../../DocumentDrawer/Provider.js'
@@ -23,7 +19,6 @@ import { MoveDocToFolder } from '../../FolderView/MoveDocToFolder/index.js'
import { Upload_v4 } from '../../Upload/index.js'
import { useFormsManager } from '../FormsManager/index.js'
import './index.scss'
import { BulkUploadProvider } from '../index.js'
const baseClass = 'collection-edit'
@@ -44,7 +39,6 @@ export function EditForm({
getDocPreferences,
hasSavePermission,
initialState,
isEditing,
isInitializing,
Upload: CustomUpload,
} = useDocumentInfo()
@@ -54,23 +48,14 @@ export function EditForm({
const { getFormState } = useServerFunctions()
const {
config: {
folders,
routes: { admin: adminRoute },
},
config: { folders },
getEntityConfig,
} = useConfig()
const abortOnChangeRef = React.useRef<AbortController>(null)
const collectionConfig = getEntityConfig({ collectionSlug: docSlug })
const router = useRouter()
const depth = useEditDepth()
const params = useSearchParams()
const { reportUpdate } = useDocumentEvents()
const { startRouteTransition } = useRouteTransition()
const locale = params.get('locale')
const collectionSlug = collectionConfig.slug
@@ -89,31 +74,9 @@ export function EditForm({
operation: 'create',
})
}
if (!isEditing && depth < 2) {
// Redirect to the same locale if it's been set
const redirectRoute = formatAdminURL({
adminRoute,
path: `/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}`,
})
startRouteTransition(() => router.push(redirectRoute))
} else {
resetUploadEdits()
}
resetUploadEdits()
},
[
adminRoute,
collectionSlug,
depth,
isEditing,
locale,
onSaveFromContext,
reportUpdate,
resetUploadEdits,
router,
startRouteTransition,
],
[collectionSlug, onSaveFromContext, reportUpdate, resetUploadEdits],
)
const onChange: NonNullable<FormProps['onChange']>[0] = useCallback(
@@ -150,54 +113,52 @@ export function EditForm({
return (
<OperationProvider operation="create">
<BulkUploadProvider>
<Form
action={action}
className={`${baseClass}__form`}
disabled={isInitializing || !hasSavePermission}
initialState={isInitializing ? undefined : initialState}
isInitializing={isInitializing}
method="POST"
onChange={[onChange]}
onSuccess={onSave}
submitted={submitted}
>
<DocumentFields
BeforeFields={
<React.Fragment>
{CustomUpload || (
<Upload_v4
collectionSlug={collectionConfig.slug}
customActions={[
folders && collectionConfig.folders && (
<MoveDocToFolder
buttonProps={{
buttonStyle: 'pill',
size: 'small',
}}
folderCollectionSlug={folders.slug}
folderFieldName={folders.fieldName}
key="move-doc-to-folder"
/>
),
].filter(Boolean)}
initialState={initialState}
resetUploadEdits={resetUploadEdits}
updateUploadEdits={updateUploadEdits}
uploadConfig={collectionConfig.upload}
uploadEdits={uploadEdits}
/>
)}
</React.Fragment>
}
docPermissions={docPermissions}
fields={collectionConfig.fields}
schemaPathSegments={[collectionConfig.slug]}
/>
<ReportAllErrors />
<GetFieldProxy />
</Form>
</BulkUploadProvider>
<Form
action={action}
className={`${baseClass}__form`}
disabled={isInitializing || !hasSavePermission}
initialState={isInitializing ? undefined : initialState}
isInitializing={isInitializing}
method="POST"
onChange={[onChange]}
onSuccess={onSave}
submitted={submitted}
>
<DocumentFields
BeforeFields={
<React.Fragment>
{CustomUpload || (
<Upload_v4
collectionSlug={collectionConfig.slug}
customActions={[
folders && collectionConfig.folders && (
<MoveDocToFolder
buttonProps={{
buttonStyle: 'pill',
size: 'small',
}}
folderCollectionSlug={folders.slug}
folderFieldName={folders.fieldName}
key="move-doc-to-folder"
/>
),
].filter(Boolean)}
initialState={initialState}
resetUploadEdits={resetUploadEdits}
updateUploadEdits={updateUploadEdits}
uploadConfig={collectionConfig.upload}
uploadEdits={uploadEdits}
/>
)}
</React.Fragment>
}
docPermissions={docPermissions}
fields={collectionConfig.fields}
schemaPathSegments={[collectionConfig.slug]}
/>
<ReportAllErrors />
<GetFieldProxy />
</Form>
</OperationProvider>
)
}

View File

@@ -11,9 +11,11 @@ import type { FormProps } from '../../../forms/Form/index.js'
import type { OnFieldSelect } from '../../FieldSelect/index.js'
import type { FieldOption } from '../../FieldSelect/reduceFieldOptions.js'
import type { State } from '../FormsManager/reducer.js'
import type { EditManyBulkUploadsProps } from './index.js'
import { Button } from '../../../elements/Button/index.js'
import { Form } from '../../../forms/Form/index.js'
import { FieldPathContext } from '../../../forms/RenderFields/context.js'
import { RenderField } from '../../../forms/RenderFields/RenderField.js'
import { XIcon } from '../../../icons/X/index.js'
import { useAuth } from '../../../providers/Auth/index.js'
@@ -22,7 +24,7 @@ import { useTranslation } from '../../../providers/Translation/index.js'
import { abortAndIgnore, handleAbortRef } from '../../../utilities/abortAndIgnore.js'
import { FieldSelect } from '../../FieldSelect/index.js'
import { useFormsManager } from '../FormsManager/index.js'
import { baseClass, type EditManyBulkUploadsProps } from './index.js'
import { baseClass } from './index.js'
import './index.scss'
import '../../../forms/RenderFields/index.scss'
@@ -169,23 +171,25 @@ export const EditManyBulkUploadsDrawerContent: React.FC<
/>
{selectedFields.length === 0 ? null : (
<div className="render-fields">
{selectedFields.map((option, i) => {
const {
value: { field, fieldPermissions, path },
} = option
<FieldPathContext value={undefined}>
{selectedFields.map((option, i) => {
const {
value: { field, fieldPermissions, path },
} = option
return (
<RenderField
clientFieldConfig={field}
indexPath=""
key={`${path}-${i}`}
parentPath=""
parentSchemaPath=""
path={path}
permissions={fieldPermissions}
/>
)
})}
return (
<RenderField
clientFieldConfig={field}
indexPath=""
key={`${path}-${i}`}
parentPath=""
parentSchemaPath=""
path={path}
permissions={fieldPermissions}
/>
)
})}
</FieldPathContext>
</div>
)}
<div className={`${baseClass}__sidebar-wrap`}>

View File

@@ -118,7 +118,7 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
const { toggleLoadingOverlay } = useLoadingOverlay()
const { closeModal } = useModal()
const { collectionSlug, drawerSlug, initialFiles, onSuccess } = useBulkUpload()
const { collectionSlug, drawerSlug, initialFiles, onSuccess, setInitialFiles } = useBulkUpload()
const [isUploading, setIsUploading] = React.useState(false)
const [loadingText, setLoadingText] = React.useState('')
@@ -366,13 +366,10 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
setIsUploading(false)
const remainingForms = []
const thumbnailIndexesToRemove = []
currentForms.forEach(({ errorCount }, i) => {
if (errorCount) {
remainingForms.push(currentForms[i])
} else {
thumbnailIndexesToRemove.push(i)
}
})
@@ -401,8 +398,13 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
totalErrorCount: remainingForms.reduce((acc, { errorCount }) => acc + errorCount, 0),
},
})
if (remainingForms.length === 0) {
setInitialFiles(undefined)
}
},
[
setInitialFiles,
actionURL,
collectionSlug,
getUploadHandler,

View File

@@ -8,6 +8,7 @@ import React from 'react'
import { toast } from 'sonner'
import { useConfig } from '../../providers/Config/index.js'
import { EditDepthProvider } from '../../providers/EditDepth/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { UploadControlsProvider } from '../../providers/UploadControls/index.js'
import { Drawer, useDrawerDepth } from '../Drawer/index.js'
@@ -77,7 +78,9 @@ export function BulkUploadDrawer() {
<Drawer gutter={false} Header={null} slug={drawerSlug}>
<FormsManagerProvider>
<UploadControlsProvider>
<DrawerContent />
<EditDepthProvider>
<DrawerContent />
</EditDepthProvider>
</UploadControlsProvider>
</FormsManagerProvider>
</Drawer>
@@ -86,61 +89,57 @@ export function BulkUploadDrawer() {
type BulkUploadContext = {
collectionSlug: string
currentActivePath: string
drawerSlug: string
initialFiles: FileList
maxFiles: number
onCancel: () => void
onSuccess: (newDocs: JsonObject[], errorCount: number) => void
setCollectionSlug: (slug: string) => void
setCurrentActivePath: (path: string) => void
setInitialFiles: (files: FileList) => void
setMaxFiles: (maxFiles: number) => void
setOnCancel: (onCancel: BulkUploadContext['onCancel']) => void
setOnSuccess: (path: string, onSuccess: BulkUploadContext['onSuccess']) => void
setOnSuccess: (onSuccess: BulkUploadContext['onSuccess']) => void
}
const Context = React.createContext<BulkUploadContext>({
collectionSlug: '',
currentActivePath: undefined,
drawerSlug: '',
initialFiles: undefined,
maxFiles: undefined,
onCancel: () => null,
onSuccess: () => null,
setCollectionSlug: () => null,
setCurrentActivePath: () => null,
setInitialFiles: () => null,
setMaxFiles: () => null,
setOnCancel: () => null,
setOnSuccess: () => null,
})
export function BulkUploadProvider({ children }: { readonly children: React.ReactNode }) {
export function BulkUploadProvider({
children,
drawerSlugPrefix,
}: {
readonly children: React.ReactNode
readonly drawerSlugPrefix?: string
}) {
const [collection, setCollection] = React.useState<string>()
const [onSuccessFunctionMap, setOnSuccessFunctionMap] =
React.useState<Record<string, BulkUploadContext['onSuccess']>>()
const [onSuccessFunction, setOnSuccessFunction] = React.useState<BulkUploadContext['onSuccess']>()
const [onCancelFunction, setOnCancelFunction] = React.useState<BulkUploadContext['onCancel']>()
const [initialFiles, setInitialFiles] = React.useState<FileList>(undefined)
const [maxFiles, setMaxFiles] = React.useState<number>(undefined)
const [currentActivePath, setCurrentActivePath] = React.useState<string>(undefined)
const drawerSlug = useBulkUploadDrawerSlug()
const drawerSlug = `${drawerSlugPrefix ? `${drawerSlugPrefix}-` : ''}${useBulkUploadDrawerSlug()}`
const setCollectionSlug: BulkUploadContext['setCollectionSlug'] = (slug) => {
setCollection(slug)
}
const setOnSuccess: BulkUploadContext['setOnSuccess'] = React.useCallback((path, onSuccess) => {
setOnSuccessFunctionMap((prev) => ({
...prev,
[path]: onSuccess,
}))
}, [])
const setOnSuccess: BulkUploadContext['setOnSuccess'] = (onSuccess) => {
setOnSuccessFunction(() => onSuccess)
}
return (
<Context
value={{
collectionSlug: collection,
currentActivePath,
drawerSlug,
initialFiles,
maxFiles,
@@ -150,13 +149,11 @@ export function BulkUploadProvider({ children }: { readonly children: React.Reac
}
},
onSuccess: (docIDs, errorCount) => {
if (onSuccessFunctionMap && Object.hasOwn(onSuccessFunctionMap, currentActivePath)) {
const onSuccessFunction = onSuccessFunctionMap[currentActivePath]
if (typeof onSuccessFunction === 'function') {
onSuccessFunction(docIDs, errorCount)
}
},
setCollectionSlug,
setCurrentActivePath,
setInitialFiles,
setMaxFiles,
setOnCancel: setOnCancelFunction,

View File

@@ -42,14 +42,16 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
const [DocumentView, setDocumentView] = useState<React.ReactNode>(undefined)
const [isLoading, setIsLoading] = useState(true)
const hasRenderedDocument = useRef(false)
const hasInitialized = useRef(false)
const getDocumentView = useCallback(
(docID?: number | string) => {
(docID?: number | string, showLoadingIndicator: boolean = false) => {
const controller = handleAbortRef(abortGetDocumentViewRef)
const fetchDocumentView = async () => {
setIsLoading(true)
if (showLoadingIndicator) {
setIsLoading(true)
}
try {
const result = await renderDocument({
@@ -141,13 +143,13 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
)
const clearDoc = useCallback(() => {
getDocumentView()
getDocumentView(undefined, true)
}, [getDocumentView])
useEffect(() => {
if (!DocumentView && !hasRenderedDocument.current) {
getDocumentView(existingDocID)
hasRenderedDocument.current = true
if (!DocumentView && !hasInitialized.current) {
getDocumentView(existingDocID, true)
hasInitialized.current = true
}
}, [DocumentView, getDocumentView, existingDocID])

View File

@@ -63,7 +63,7 @@ export const StaticFileDetails: React.FC<StaticFileDetailsProps> = (props) => {
width={width as number}
/>
{(enableAdjustments || customUploadActions) && (
{(enableAdjustments || (hasImageSizes && doc.filename) || customUploadActions) && (
<UploadActions
customActions={customUploadActions}
enableAdjustments={Boolean(enableAdjustments)}

View File

@@ -27,12 +27,7 @@ export function ListBulkUploadButton({
*/
openBulkUpload?: () => void
}) {
const {
drawerSlug: bulkUploadDrawerSlug,
setCollectionSlug,
setCurrentActivePath,
setOnSuccess,
} = useBulkUpload()
const { drawerSlug: bulkUploadDrawerSlug, setCollectionSlug, setOnSuccess } = useBulkUpload()
const { t } = useTranslation()
const { openModal } = useModal()
const router = useRouter()
@@ -42,9 +37,8 @@ export function ListBulkUploadButton({
openBulkUploadFromProps()
} else {
setCollectionSlug(collectionSlug)
setCurrentActivePath(collectionSlug)
openModal(bulkUploadDrawerSlug)
setOnSuccess(collectionSlug, () => {
setOnSuccess(() => {
if (typeof onBulkUploadSuccess === 'function') {
onBulkUploadSuccess()
} else {
@@ -58,7 +52,6 @@ export function ListBulkUploadButton({
bulkUploadDrawerSlug,
openModal,
setCollectionSlug,
setCurrentActivePath,
setOnSuccess,
onBulkUploadSuccess,
openBulkUploadFromProps,

View File

@@ -304,15 +304,16 @@ export const Upload_v4: React.FC<UploadProps_v4> = (props) => {
setUploadStatus('failed')
}
}, [
fileUrl,
uploadConfig,
setUploadStatus,
handleFileChange,
useServerSideFetch,
api,
collectionSlug,
fileUrl,
handleFileChange,
id,
serverURL,
api,
setUploadStatus,
uploadConfig,
uploadControlFileName,
useServerSideFetch,
])
useEffect(() => {

View File

@@ -185,7 +185,7 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
onClick={async () => {
await addCondition({
andIndex: 0,
field: reducedFields[0],
field: reducedFields.find((field) => !field.field.admin?.disableListFilter),
orIndex: conditions.length,
relation: 'or',
})

View File

@@ -779,7 +779,7 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
// and when the devtools are closed. Temporary solution, we can probably do better.
setTimeout(() => {
openListDrawer()
}, 50)
}, 100)
} else if (appearance === 'select') {
setMenuIsOpen(true)
if (!hasLoadedFirstPageRef.current) {

View File

@@ -118,14 +118,8 @@ export function UploadInput(props: UploadInputProps) {
)
const { openModal } = useModal()
const {
drawerSlug,
setCollectionSlug,
setCurrentActivePath,
setInitialFiles,
setMaxFiles,
setOnSuccess,
} = useBulkUpload()
const { drawerSlug, setCollectionSlug, setInitialFiles, setMaxFiles, setOnSuccess } =
useBulkUpload()
const { permissions } = useAuth()
const { code } = useLocale()
const { i18n, t } = useTranslation()
@@ -294,7 +288,6 @@ export function UploadInput(props: UploadInputProps) {
if (typeof maxRows === 'number') {
setMaxFiles(maxRows)
}
setCurrentActivePath(path)
openModal(drawerSlug)
},
[
@@ -306,8 +299,6 @@ export function UploadInput(props: UploadInputProps) {
setInitialFiles,
maxRows,
setMaxFiles,
path,
setCurrentActivePath,
],
)
@@ -461,7 +452,7 @@ export function UploadInput(props: UploadInputProps) {
}, [populateDocs, activeRelationTo, value])
useEffect(() => {
setOnSuccess(path, onUploadSuccess)
setOnSuccess(onUploadSuccess)
}, [value, path, onUploadSuccess, setOnSuccess])
const showDropzone =

View File

@@ -4,12 +4,13 @@ import type { UploadFieldClientProps } from 'payload'
import React, { useMemo } from 'react'
import { BulkUploadProvider } from '../../elements/BulkUpload/index.js'
import { useField } from '../../forms/useField/index.js'
import { withCondition } from '../../forms/withCondition/index.js'
import { useConfig } from '../../providers/Config/index.js'
import './index.scss'
import { mergeFieldStyles } from '../mergeFieldStyles.js'
import { UploadInput } from './Input.js'
import './index.scss'
export { UploadInput } from './Input.js'
export type { UploadInputProps } from './Input.js'
@@ -62,33 +63,35 @@ export function UploadComponent(props: UploadFieldClientProps) {
const styles = useMemo(() => mergeFieldStyles(field), [field])
return (
<UploadInput
AfterInput={AfterInput}
allowCreate={allowCreate !== false}
api={config.routes.api}
BeforeInput={BeforeInput}
className={className}
Description={Description}
description={description}
displayPreview={displayPreview}
Error={Error}
filterOptions={filterOptions}
hasMany={hasMany}
isSortable={isSortable}
label={label}
Label={Label}
localized={localized}
maxRows={maxRows}
onChange={setValue}
path={path}
readOnly={readOnly || disabled}
relationTo={relationTo}
required={required}
serverURL={config.serverURL}
showError={showError}
style={styles}
value={value}
/>
<BulkUploadProvider drawerSlugPrefix={pathFromProps}>
<UploadInput
AfterInput={AfterInput}
allowCreate={allowCreate !== false}
api={config.routes.api}
BeforeInput={BeforeInput}
className={className}
Description={Description}
description={description}
displayPreview={displayPreview}
Error={Error}
filterOptions={filterOptions}
hasMany={hasMany}
isSortable={isSortable}
label={label}
Label={Label}
localized={localized}
maxRows={maxRows}
onChange={setValue}
path={path}
readOnly={readOnly || disabled}
relationTo={relationTo}
required={required}
serverURL={config.serverURL}
showError={showError}
style={styles}
value={value}
/>
</BulkUploadProvider>
)
}

View File

@@ -96,31 +96,10 @@ export const useField = <TValue,>(options?: Options): FieldType<TValue> => {
})
if (!disableModifyingForm) {
if (typeof setModified === 'function') {
// Only update setModified to true if the form is not already set to modified. Otherwise the following could happen:
// 1. Text field: someone types in it in an unmodified form
// 2. After setTimeout triggers setModified(true): form is set to modified. Save Button becomes available. Good!
// 3. Type something in text field
// 4. Click on save button before setTimeout in useField has finished (so setModified(true) has not been run yet)
// 5. Form is saved, setModified(false) is set in the Form/index.tsx `submit` function, "saved successfully" toast appears
// 6. setModified(true) inside the timeout is run, form is set to modified again, even though it was already saved and thus set to unmodified. Bad! This should have happened before the form is saved. Now the form should be unmodified and stay that way
// until a NEW change happens. Due to this, the "Leave without saving" modal appears even though it should not when leaving the page fast immediately after saving the document.
// This is only an issue for forms which have already been set to modified true, as that causes the save button to be enabled. If we prevent this setTimeout to be run
// for already-modified forms first place (which is unnecessary), we can avoid this issue. As for unmodified forms, this race issue will not happen, because you cannot click the save button faster
// than the timeout in useField is run. That's because the save button won't even be enabled for clicking until the setTimeout in useField has run.
// This fixes e2e test flakes, as e2e tests were often so fast that they were saving the form before the timeout in useField has run.
// Specifically, this fixes the 'should not warn about unsaved changes when navigating to lexical editor with blocks node and then leaving the page after making a change and saving' lexical e2e test.
if (modified === false) {
// Update modified state after field value comes back
// to avoid cursor jump caused by state value / DOM mismatch
setTimeout(() => {
setModified(true)
}, 10)
}
}
setModified(true)
}
},
[setModified, path, dispatchField, disableFormData, hasRows, modified],
[setModified, path, dispatchField, disableFormData, hasRows],
)
// Store result from hook as ref

View File

@@ -0,0 +1,28 @@
import { useCallback, useEffect, useRef, useState } from 'react'
/**
* A hook for managing state that can be controlled by props but also overridden locally.
* Props always take precedence if they change, but local state can override them temporarily.
*/
export function useControllableState<T>(
propValue: T,
defaultValue?: T,
): [T, (value: ((prev: T) => T) | T) => void] {
const [localValue, setLocalValue] = useState<T>(propValue ?? defaultValue)
const initialRenderRef = useRef(true)
useEffect(() => {
if (initialRenderRef.current) {
initialRenderRef.current = false
return
}
setLocalValue(propValue)
}, [propValue])
const setValue = useCallback((value: ((prev: T) => T) | T) => {
setLocalValue(value)
}, [])
return [localValue, setValue]
}

View File

@@ -14,7 +14,7 @@ type QueuedTaskOptions = {
* Can also be used to perform side effects before processing the queue
* @returns {boolean} If `false`, the queue will not process
*/
beforeProcess?: () => boolean
beforeProcess?: () => boolean | void
}
type QueueTask = (fn: QueuedFunction, options?: QueuedTaskOptions) => void

View File

@@ -1,9 +1,10 @@
'use client'
import type { ClientUser, DocumentPreferences, SanitizedDocumentPermissions } from 'payload'
import type { ClientUser, DocumentPreferences } from 'payload'
import * as qs from 'qs-esm'
import React, { createContext, use, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useControllableState } from '../../hooks/useControllableState.js'
import { useAuth } from '../../providers/Auth/index.js'
import { requests } from '../../utilities/api.js'
import { formatDocTitle } from '../../utilities/formatDocTitle/index.js'
@@ -45,12 +46,11 @@ const DocumentInfo: React.FC<
versionCount: versionCountFromProps,
} = props
const [docPermissions, setDocPermissions] =
useState<SanitizedDocumentPermissions>(docPermissionsFromProps)
const [docPermissions, setDocPermissions] = useControllableState(docPermissionsFromProps)
const [hasSavePermission, setHasSavePermission] = useState<boolean>(hasSavePermissionFromProps)
const [hasSavePermission, setHasSavePermission] = useControllableState(hasSavePermissionFromProps)
const [hasPublishPermission, setHasPublishPermission] = useState<boolean>(
const [hasPublishPermission, setHasPublishPermission] = useControllableState(
hasPublishPermissionFromProps,
)
@@ -101,15 +101,24 @@ const DocumentInfo: React.FC<
unpublishedVersionCountFromProps,
)
const [documentIsLocked, setDocumentIsLocked] = useState<boolean | undefined>(isLockedFromProps)
const [currentEditor, setCurrentEditor] = useState<ClientUser | null>(currentEditorFromProps)
const [lastUpdateTime, setLastUpdateTime] = useState<number>(lastUpdateTimeFromProps)
const [savedDocumentData, setSavedDocumentData] = useState(initialData)
const [uploadStatus, setUploadStatus] = useState<'failed' | 'idle' | 'uploading'>('idle')
const [documentIsLocked, setDocumentIsLocked] = useControllableState<boolean | undefined>(
isLockedFromProps,
)
const [currentEditor, setCurrentEditor] = useControllableState<ClientUser | null>(
currentEditorFromProps,
)
const [lastUpdateTime, setLastUpdateTime] = useControllableState<number>(lastUpdateTimeFromProps)
const [savedDocumentData, setSavedDocumentData] = useControllableState(initialData)
const [uploadStatus, setUploadStatus] = useControllableState<'failed' | 'idle' | 'uploading'>(
'idle',
)
const updateUploadStatus = useCallback((status: 'failed' | 'idle' | 'uploading') => {
setUploadStatus(status)
}, [])
const updateUploadStatus = useCallback(
(status: 'failed' | 'idle' | 'uploading') => {
setUploadStatus(status)
},
[setUploadStatus],
)
const { getPreference, setPreference } = usePreferences()
const { code: locale } = useLocale()
@@ -170,7 +179,7 @@ const DocumentInfo: React.FC<
console.error('Failed to unlock the document', error)
}
},
[serverURL, api, globalSlug],
[serverURL, api, globalSlug, setDocumentIsLocked],
)
const updateDocumentEditor = useCallback(
@@ -279,7 +288,7 @@ const DocumentInfo: React.FC<
(json) => {
setSavedDocumentData(json)
},
[],
[setSavedDocumentData],
)
/**

View File

@@ -226,6 +226,13 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = ({
)
}, [isLivePreviewing, setPreference, collectionSlug, globalSlug])
const isLivePreviewEnabled = Boolean(
operation !== 'create' &&
((collectionSlug && config?.admin?.livePreview?.collections?.includes(collectionSlug)) ||
(globalSlug && config.admin?.livePreview?.globals?.includes(globalSlug)) ||
entityConfig?.admin?.livePreview),
)
return (
<LivePreviewContext
value={{
@@ -235,13 +242,7 @@ export const LivePreviewProvider: React.FC<LivePreviewProviderProps> = ({
fieldSchemaJSON,
iframeHasLoaded,
iframeRef,
isLivePreviewEnabled: Boolean(
(operation !== 'create' &&
collectionSlug &&
config?.admin?.livePreview?.collections?.includes(collectionSlug)) ||
(globalSlug && config.admin?.livePreview?.globals?.includes(globalSlug)) ||
entityConfig?.admin?.livePreview,
),
isLivePreviewEnabled,
isLivePreviewing,
isPopupOpen,
listeningForMessages,

View File

@@ -69,12 +69,13 @@ export const Auth: React.FC<Props> = (props) => {
})
if (operation === 'create') {
showPasswordFields = typeof passwordPermissions === 'object' && passwordPermissions.create
showPasswordFields =
passwordPermissions === true ||
(typeof passwordPermissions === 'object' && passwordPermissions.create)
} else {
showPasswordFields =
typeof passwordPermissions === 'object' &&
passwordPermissions.read &&
passwordPermissions.update
passwordPermissions === true ||
(typeof passwordPermissions === 'object' && passwordPermissions.update)
}
}

View File

@@ -86,12 +86,7 @@ export function DefaultListView(props: ListViewClientProps) {
} = useListQuery()
const { openModal } = useModal()
const {
drawerSlug: bulkUploadDrawerSlug,
setCollectionSlug,
setCurrentActivePath,
setOnSuccess,
} = useBulkUpload()
const { drawerSlug: bulkUploadDrawerSlug, setCollectionSlug, setOnSuccess } = useBulkUpload()
const collectionConfig = getEntityConfig({ collectionSlug })
@@ -124,18 +119,9 @@ export function DefaultListView(props: ListViewClientProps) {
const openBulkUpload = React.useCallback(() => {
setCollectionSlug(collectionSlug)
setCurrentActivePath(collectionSlug)
openModal(bulkUploadDrawerSlug)
setOnSuccess(collectionSlug, () => router.refresh())
}, [
router,
collectionSlug,
bulkUploadDrawerSlug,
openModal,
setCollectionSlug,
setCurrentActivePath,
setOnSuccess,
])
setOnSuccess(() => router.refresh())
}, [router, collectionSlug, bulkUploadDrawerSlug, openModal, setCollectionSlug, setOnSuccess])
useEffect(() => {
if (!isInDrawer) {

322
pnpm-lock.yaml generated
View File

@@ -44,7 +44,7 @@ importers:
version: 1.50.0
'@sentry/nextjs':
specifier: ^8.33.1
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29)(esbuild@0.25.5))
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))
'@sentry/node':
specifier: ^8.33.1
version: 8.37.1
@@ -97,11 +97,11 @@ importers:
specifier: 16.4.7
version: 16.4.7
drizzle-kit:
specifier: 0.31.0
version: 0.31.0
specifier: 0.31.4
version: 0.31.4
drizzle-orm:
specifier: 0.43.1
version: 0.43.1(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@vercel/postgres@0.9.0)(pg@8.11.3)
specifier: 0.44.2
version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3)
escape-html:
specifier: ^1.0.3
version: 1.0.3
@@ -142,8 +142,8 @@ importers:
specifier: ^5.0.0
version: 5.0.0
pg:
specifier: 8.11.3
version: 8.11.3
specifier: 8.16.3
version: 8.16.3
playwright:
specifier: 1.50.0
version: 1.50.0
@@ -319,14 +319,14 @@ importers:
specifier: 2.12.1
version: 2.12.1
drizzle-kit:
specifier: 0.31.1
version: 0.31.1
specifier: 0.31.4
version: 0.31.4
drizzle-orm:
specifier: 0.44.2
version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(pg@8.11.3)
version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3)
pg:
specifier: 8.11.3
version: 8.11.3
specifier: 8.16.3
version: 8.16.3
prompts:
specifier: 2.4.2
version: 2.4.2
@@ -365,11 +365,11 @@ importers:
specifier: 2.12.1
version: 2.12.1
drizzle-kit:
specifier: 0.31.1
version: 0.31.1
specifier: 0.31.4
version: 0.31.4
drizzle-orm:
specifier: 0.44.2
version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(pg@8.11.3)
version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3)
prompts:
specifier: 2.4.2
version: 2.4.2
@@ -408,14 +408,14 @@ importers:
specifier: 2.12.1
version: 2.12.1
drizzle-kit:
specifier: 0.31.1
version: 0.31.1
specifier: 0.31.4
version: 0.31.4
drizzle-orm:
specifier: 0.44.2
version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(pg@8.11.3)
version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3)
pg:
specifier: 8.11.3
version: 8.11.3
specifier: 8.16.3
version: 8.16.3
prompts:
specifier: 2.4.2
version: 2.4.2
@@ -455,7 +455,7 @@ importers:
version: 2.0.3
drizzle-orm:
specifier: 0.44.2
version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(pg@8.11.3)
version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3)
prompts:
specifier: 2.4.2
version: 2.4.2
@@ -1143,7 +1143,7 @@ importers:
dependencies:
'@sentry/nextjs':
specifier: ^8.33.1
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29)(esbuild@0.25.5))
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))
'@sentry/types':
specifier: ^8.33.1
version: 8.37.1
@@ -2045,7 +2045,7 @@ importers:
version: link:../packages/ui
'@sentry/nextjs':
specifier: ^8.33.1
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29)(esbuild@0.25.5))
version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))
'@sentry/react':
specifier: ^7.77.0
version: 7.119.2(react@19.1.0)
@@ -2077,11 +2077,11 @@ importers:
specifier: 16.4.7
version: 16.4.7
drizzle-kit:
specifier: 0.31.1
version: 0.31.1
specifier: 0.31.4
version: 0.31.4
drizzle-orm:
specifier: 0.44.2
version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@vercel/postgres@0.9.0)(pg@8.11.3)
version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3)
escape-html:
specifier: 1.0.3
version: 1.0.3
@@ -2116,8 +2116,8 @@ importers:
specifier: workspace:*
version: link:../packages/payload
pg:
specifier: 8.11.3
version: 8.11.3
specifier: 8.16.3
version: 8.16.3
qs-esm:
specifier: 7.0.2
version: 7.0.2
@@ -5040,6 +5040,9 @@ packages:
cpu: [x64]
os: [win32]
'@petamoriken/float16@3.9.2':
resolution: {integrity: sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -7057,10 +7060,6 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
buffer-writer@2.0.0:
resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==}
engines: {node: '>=4'}
buffer@4.9.2:
resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==}
@@ -7651,103 +7650,10 @@ packages:
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
engines: {node: '>=12'}
drizzle-kit@0.31.0:
resolution: {integrity: sha512-pcKVT+GbfPA+bUovPIilgVOoq+onNBo/YQBG86sf3/GFHkN6lRJPm1l7dKN0IMAk57RQoIm4GUllRrasLlcaSg==}
drizzle-kit@0.31.4:
resolution: {integrity: sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==}
hasBin: true
drizzle-kit@0.31.1:
resolution: {integrity: sha512-PUjYKWtzOzPtdtQlTHQG3qfv4Y0XT8+Eas6UbxCmxTj7qgMf+39dDujf1BP1I+qqZtw9uzwTh8jYtkMuCq+B0Q==}
hasBin: true
drizzle-orm@0.43.1:
resolution: {integrity: sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA==}
peerDependencies:
'@aws-sdk/client-rds-data': '>=3'
'@cloudflare/workers-types': '>=4'
'@electric-sql/pglite': '>=0.2.0'
'@libsql/client': '>=0.10.0'
'@libsql/client-wasm': '>=0.10.0'
'@neondatabase/serverless': '>=0.10.0'
'@op-engineering/op-sqlite': '>=2'
'@opentelemetry/api': ^1.4.1
'@planetscale/database': '>=1.13'
'@prisma/client': '*'
'@tidbcloud/serverless': '*'
'@types/better-sqlite3': '*'
'@types/pg': '*'
'@types/sql.js': '*'
'@vercel/postgres': '>=0.8.0'
'@xata.io/client': '*'
better-sqlite3: '>=7'
bun-types: '*'
expo-sqlite: '>=14.0.0'
gel: '>=2'
knex: '*'
kysely: '*'
mysql2: '>=2'
pg: '>=8'
postgres: '>=3'
prisma: '*'
sql.js: '>=1'
sqlite3: '>=5'
peerDependenciesMeta:
'@aws-sdk/client-rds-data':
optional: true
'@cloudflare/workers-types':
optional: true
'@electric-sql/pglite':
optional: true
'@libsql/client':
optional: true
'@libsql/client-wasm':
optional: true
'@neondatabase/serverless':
optional: true
'@op-engineering/op-sqlite':
optional: true
'@opentelemetry/api':
optional: true
'@planetscale/database':
optional: true
'@prisma/client':
optional: true
'@tidbcloud/serverless':
optional: true
'@types/better-sqlite3':
optional: true
'@types/pg':
optional: true
'@types/sql.js':
optional: true
'@vercel/postgres':
optional: true
'@xata.io/client':
optional: true
better-sqlite3:
optional: true
bun-types:
optional: true
expo-sqlite:
optional: true
gel:
optional: true
knex:
optional: true
kysely:
optional: true
mysql2:
optional: true
pg:
optional: true
postgres:
optional: true
prisma:
optional: true
sql.js:
optional: true
sqlite3:
optional: true
drizzle-orm@0.44.2:
resolution: {integrity: sha512-zGAqBzWWkVSFjZpwPOrmCrgO++1kZ5H/rZ4qTGeGOe18iXGVJWf3WPfHOVwFIbmi8kHjfJstC6rJomzGx8g/dQ==}
peerDependencies:
@@ -7893,6 +7799,10 @@ packages:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
env-paths@3.0.0:
resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
environment@1.1.0:
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
engines: {node: '>=18'}
@@ -8574,6 +8484,11 @@ packages:
peerDependencies:
next: '>=13.2.0'
gel@2.0.1:
resolution: {integrity: sha512-gfem3IGvqKqXwEq7XseBogyaRwGsQGuE7Cw/yQsjLGdgiyqX92G1xENPCE0ltunPGcsJIa6XBOTx/PK169mOqw==}
engines: {node: '>= 18.0.0'}
hasBin: true
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -9210,6 +9125,10 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
isexe@3.1.1:
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
engines: {node: '>=16'}
isomorphic-unfetch@3.1.0:
resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==}
@@ -10282,9 +10201,6 @@ packages:
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
packet-reader@1.0.0:
resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -10357,11 +10273,11 @@ packages:
perfect-debounce@1.0.0:
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
pg-cloudflare@1.1.1:
resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==}
pg-cloudflare@1.2.7:
resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==}
pg-connection-string@2.7.0:
resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==}
pg-connection-string@2.9.1:
resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==}
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
@@ -10371,11 +10287,17 @@ packages:
resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==}
engines: {node: '>=4'}
pg-pool@3.7.0:
resolution: {integrity: sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==}
pg-pool@3.10.1:
resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==}
peerDependencies:
pg: '>=8.0'
pg-protocol@1.10.2:
resolution: {integrity: sha512-Ci7jy8PbaWxfsck2dwZdERcDG2A0MG8JoQILs+uZNjABFuBuItAZCWUNz8sXRDMoui24rJw7WlXqgpMdBSN/vQ==}
pg-protocol@1.10.3:
resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==}
pg-protocol@1.7.0:
resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==}
@@ -10387,9 +10309,9 @@ packages:
resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==}
engines: {node: '>=10'}
pg@8.11.3:
resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==}
engines: {node: '>= 8.0.0'}
pg@8.16.3:
resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==}
engines: {node: '>= 16.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
peerDependenciesMeta:
@@ -11216,6 +11138,10 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
shell-quote@1.8.3:
resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
engines: {node: '>= 0.4'}
shelljs@0.8.5:
resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==}
engines: {node: '>=4'}
@@ -12351,6 +12277,11 @@ packages:
engines: {node: '>= 8'}
hasBin: true
which@4.0.0:
resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==}
engines: {node: ^16.13.0 || >=18.0.0}
hasBin: true
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
@@ -15972,6 +15903,9 @@ snapshots:
'@oxc-resolver/binding-win32-x64-msvc@5.3.0':
optional: true
'@petamoriken/float16@3.9.2':
optional: true
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -16466,7 +16400,7 @@ snapshots:
'@sentry/utils': 7.119.2
localforage: 1.10.0
'@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29)(esbuild@0.25.5))':
'@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation-http': 0.53.0(@opentelemetry/api@1.9.0)
@@ -16480,7 +16414,7 @@ snapshots:
'@sentry/types': 8.37.1
'@sentry/utils': 8.37.1
'@sentry/vercel-edge': 8.37.1
'@sentry/webpack-plugin': 2.22.6(webpack@5.96.1(@swc/core@1.11.29)(esbuild@0.25.5))
'@sentry/webpack-plugin': 2.22.6(webpack@5.96.1(@swc/core@1.11.29))
chalk: 3.0.0
next: 15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4)
resolve: 1.22.8
@@ -16495,7 +16429,7 @@ snapshots:
- supports-color
- webpack
'@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29)(esbuild@0.25.5))':
'@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/instrumentation-http': 0.53.0(@opentelemetry/api@1.9.0)
@@ -16509,7 +16443,7 @@ snapshots:
'@sentry/types': 8.37.1
'@sentry/utils': 8.37.1
'@sentry/vercel-edge': 8.37.1
'@sentry/webpack-plugin': 2.22.6(webpack@5.96.1(@swc/core@1.11.29)(esbuild@0.25.5))
'@sentry/webpack-plugin': 2.22.6(webpack@5.96.1(@swc/core@1.11.29))
chalk: 3.0.0
next: 15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4)
resolve: 1.22.8
@@ -16619,12 +16553,12 @@ snapshots:
'@sentry/types': 8.37.1
'@sentry/utils': 8.37.1
'@sentry/webpack-plugin@2.22.6(webpack@5.96.1(@swc/core@1.11.29)(esbuild@0.25.5))':
'@sentry/webpack-plugin@2.22.6(webpack@5.96.1(@swc/core@1.11.29))':
dependencies:
'@sentry/bundler-plugin-core': 2.22.6
unplugin: 1.0.1
uuid: 9.0.0
webpack: 5.96.1(@swc/core@1.11.29)(esbuild@0.25.5)
webpack: 5.96.1(@swc/core@1.11.29)
transitivePeerDependencies:
- encoding
- supports-color
@@ -17481,13 +17415,13 @@ snapshots:
'@types/pg@8.11.6':
dependencies:
'@types/node': 22.15.30
pg-protocol: 1.7.0
pg-protocol: 1.10.2
pg-types: 4.0.2
'@types/pg@8.6.1':
dependencies:
'@types/node': 22.15.30
pg-protocol: 1.7.0
pg-protocol: 1.10.2
pg-types: 2.2.0
'@types/pluralize@0.0.33': {}
@@ -18477,8 +18411,6 @@ snapshots:
buffer-from@1.1.2: {}
buffer-writer@2.0.0: {}
buffer@4.9.2:
dependencies:
base64-js: 1.5.1
@@ -19042,7 +18974,7 @@ snapshots:
dotenv@16.4.7: {}
drizzle-kit@0.31.0:
drizzle-kit@0.31.4:
dependencies:
'@drizzle-team/brocli': 0.10.2
'@esbuild-kit/esm-loader': 2.6.5
@@ -19051,38 +18983,23 @@ snapshots:
transitivePeerDependencies:
- supports-color
drizzle-kit@0.31.1:
dependencies:
'@drizzle-team/brocli': 0.10.2
'@esbuild-kit/esm-loader': 2.6.5
esbuild: 0.25.5
esbuild-register: 3.6.0(esbuild@0.25.5)
transitivePeerDependencies:
- supports-color
drizzle-orm@0.43.1(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@vercel/postgres@0.9.0)(pg@8.11.3):
optionalDependencies:
'@libsql/client': 0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5)
'@opentelemetry/api': 1.9.0
'@types/pg': 8.11.6
'@vercel/postgres': 0.9.0
pg: 8.11.3
drizzle-orm@0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(pg@8.11.3):
drizzle-orm@0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3):
optionalDependencies:
'@libsql/client': 0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5)
'@opentelemetry/api': 1.9.0
'@types/pg': 8.10.2
'@vercel/postgres': 0.9.0
pg: 8.11.3
gel: 2.0.1
pg: 8.16.3
drizzle-orm@0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@vercel/postgres@0.9.0)(pg@8.11.3):
drizzle-orm@0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3):
optionalDependencies:
'@libsql/client': 0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5)
'@opentelemetry/api': 1.9.0
'@types/pg': 8.11.6
'@vercel/postgres': 0.9.0
pg: 8.11.3
gel: 2.0.1
pg: 8.16.3
dunder-proto@1.0.1:
dependencies:
@@ -19134,6 +19051,9 @@ snapshots:
entities@6.0.1: {}
env-paths@3.0.0:
optional: true
environment@1.1.0: {}
error-ex@1.3.2:
@@ -19318,7 +19238,7 @@ snapshots:
esbuild-register@3.6.0(esbuild@0.25.5):
dependencies:
debug: 4.3.7
debug: 4.4.1
esbuild: 0.25.5
transitivePeerDependencies:
- supports-color
@@ -20157,6 +20077,18 @@ snapshots:
dependencies:
next: 15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4)
gel@2.0.1:
dependencies:
'@petamoriken/float16': 3.9.2
debug: 4.4.1
env-paths: 3.0.0
semver: 7.7.1
shell-quote: 1.8.3
which: 4.0.0
transitivePeerDependencies:
- supports-color
optional: true
gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {}
@@ -20801,6 +20733,9 @@ snapshots:
isexe@2.0.0: {}
isexe@3.1.1:
optional: true
isomorphic-unfetch@3.1.0:
dependencies:
node-fetch: 2.7.0
@@ -22317,8 +22252,6 @@ snapshots:
package-json-from-dist@1.0.1: {}
packet-reader@1.0.0: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -22383,18 +22316,22 @@ snapshots:
perfect-debounce@1.0.0: {}
pg-cloudflare@1.1.1:
pg-cloudflare@1.2.7:
optional: true
pg-connection-string@2.7.0: {}
pg-connection-string@2.9.1: {}
pg-int8@1.0.1: {}
pg-numeric@1.0.2: {}
pg-pool@3.7.0(pg@8.11.3):
pg-pool@3.10.1(pg@8.16.3):
dependencies:
pg: 8.11.3
pg: 8.16.3
pg-protocol@1.10.2: {}
pg-protocol@1.10.3: {}
pg-protocol@1.7.0: {}
@@ -22416,17 +22353,15 @@ snapshots:
postgres-interval: 3.0.0
postgres-range: 1.1.4
pg@8.11.3:
pg@8.16.3:
dependencies:
buffer-writer: 2.0.0
packet-reader: 1.0.0
pg-connection-string: 2.7.0
pg-pool: 3.7.0(pg@8.11.3)
pg-protocol: 1.7.0
pg-connection-string: 2.9.1
pg-pool: 3.10.1(pg@8.16.3)
pg-protocol: 1.10.3
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.1.1
pg-cloudflare: 1.2.7
pgpass@1.0.5:
dependencies:
@@ -23326,6 +23261,9 @@ snapshots:
shebang-regex@3.0.0: {}
shell-quote@1.8.3:
optional: true
shelljs@0.8.5:
dependencies:
glob: 7.2.3
@@ -23901,17 +23839,16 @@ snapshots:
ansi-escapes: 4.3.2
supports-hyperlinks: 2.3.0
terser-webpack-plugin@5.3.10(@swc/core@1.11.29)(esbuild@0.25.5)(webpack@5.96.1(@swc/core@1.11.29)(esbuild@0.25.5)):
terser-webpack-plugin@5.3.10(@swc/core@1.11.29)(webpack@5.96.1(@swc/core@1.11.29)):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
jest-worker: 27.5.1
schema-utils: 3.3.0
serialize-javascript: 6.0.2
terser: 5.36.0
webpack: 5.96.1(@swc/core@1.11.29)(esbuild@0.25.5)
webpack: 5.96.1(@swc/core@1.11.29)
optionalDependencies:
'@swc/core': 1.11.29
esbuild: 0.25.5
terser@5.36.0:
dependencies:
@@ -24618,7 +24555,7 @@ snapshots:
webpack-virtual-modules@0.5.0: {}
webpack@5.96.1(@swc/core@1.11.29)(esbuild@0.25.5):
webpack@5.96.1(@swc/core@1.11.29):
dependencies:
'@types/eslint-scope': 3.7.7
'@types/estree': 1.0.7
@@ -24640,7 +24577,7 @@ snapshots:
neo-async: 2.6.2
schema-utils: 3.3.0
tapable: 2.2.1
terser-webpack-plugin: 5.3.10(@swc/core@1.11.29)(esbuild@0.25.5)(webpack@5.96.1(@swc/core@1.11.29)(esbuild@0.25.5))
terser-webpack-plugin: 5.3.10(@swc/core@1.11.29)(webpack@5.96.1(@swc/core@1.11.29))
watchpack: 2.4.2
webpack-sources: 3.2.3
transitivePeerDependencies:
@@ -24734,6 +24671,11 @@ snapshots:
dependencies:
isexe: 2.0.0
which@4.0.0:
dependencies:
isexe: 3.1.1
optional: true
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0

View File

@@ -60,7 +60,7 @@ const BeforeDashboard: React.FC = () => {
</ul>
{'Pro Tip: This block is a '}
<a
href="https://payloadcms.com/docs/admin/custom-components/overview#base-component-overrides"
href="https://payloadcms.com/docs/custom-components/overview"
rel="noopener noreferrer"
target="_blank"
>

View File

@@ -24,10 +24,10 @@ export default buildConfig({
admin: {
components: {
// The `BeforeLogin` component renders a message that you see while logging into your admin panel.
// Feel free to delete this at any time. Simply remove the line below and the import `BeforeLogin` statement on line 15.
// Feel free to delete this at any time. Simply remove the line below.
beforeLogin: ['@/components/BeforeLogin'],
// The `BeforeDashboard` component renders the 'welcome' block that you see after logging into your admin panel.
// Feel free to delete this at any time. Simply remove the line below and the import `BeforeDashboard` statement on line 15.
// Feel free to delete this at any time. Simply remove the line below.
beforeDashboard: ['@/components/BeforeDashboard'],
},
importMap: {

View File

@@ -60,7 +60,7 @@ const BeforeDashboard: React.FC = () => {
</ul>
{'Pro Tip: This block is a '}
<a
href="https://payloadcms.com/docs/admin/custom-components/overview#base-component-overrides"
href="https://payloadcms.com/docs/custom-components/overview"
rel="noopener noreferrer"
target="_blank"
>

View File

@@ -24,10 +24,10 @@ export default buildConfig({
admin: {
components: {
// The `BeforeLogin` component renders a message that you see while logging into your admin panel.
// Feel free to delete this at any time. Simply remove the line below and the import `BeforeLogin` statement on line 15.
// Feel free to delete this at any time. Simply remove the line below.
beforeLogin: ['@/components/BeforeLogin'],
// The `BeforeDashboard` component renders the 'welcome' block that you see after logging into your admin panel.
// Feel free to delete this at any time. Simply remove the line below and the import `BeforeDashboard` statement on line 15.
// Feel free to delete this at any time. Simply remove the line below.
beforeDashboard: ['@/components/BeforeDashboard'],
},
importMap: {

View File

@@ -203,6 +203,13 @@ export interface User {
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
sessions?:
| {
id: string;
createdAt?: string | null;
expiresAt: string;
}[]
| null;
password?: string | null;
}
/**
@@ -341,6 +348,13 @@ export interface UsersSelect<T extends boolean = true> {
hash?: T;
loginAttempts?: T;
lockUntil?: T;
sessions?:
| T
| {
id?: T;
createdAt?: T;
expiresAt?: T;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema

View File

@@ -436,10 +436,15 @@ describe('Access Control', () => {
const documentDrawer = page.locator(`[id^=doc-drawer_${createNotUpdateCollectionSlug}_1_]`)
await expect(documentDrawer).toBeVisible()
await expect(documentDrawer.locator('#action-save')).toBeVisible()
await documentDrawer.locator('#field-name').fill('name')
await expect(documentDrawer.locator('#field-name')).toHaveValue('name')
await documentDrawer.locator('#action-save').click()
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
await saveDocAndAssert(
page,
`[id^=doc-drawer_${createNotUpdateCollectionSlug}_1_] #action-save`,
)
await expect(documentDrawer.locator('#action-save')).toBeHidden()
await expect(documentDrawer.locator('#field-name')).toBeDisabled()
})

View File

@@ -95,7 +95,6 @@ export interface Config {
'auth-collection': AuthCollection;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-sessions': PayloadSession;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
@@ -126,7 +125,6 @@ export interface Config {
'auth-collection': AuthCollectionSelect<false> | AuthCollectionSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-sessions': PayloadSessionsSelect<false> | PayloadSessionsSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
@@ -232,6 +230,13 @@ export interface User {
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
sessions?:
| {
id: string;
createdAt?: string | null;
expiresAt: string;
}[]
| null;
password?: string | null;
}
/**
@@ -249,6 +254,13 @@ export interface PublicUser {
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
sessions?:
| {
id: string;
createdAt?: string | null;
expiresAt: string;
}[]
| null;
password?: string | null;
}
/**
@@ -740,6 +752,13 @@ export interface AuthCollection {
_verificationToken?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
sessions?:
| {
id: string;
createdAt?: string | null;
expiresAt: string;
}[]
| null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -893,26 +912,6 @@ export interface PayloadPreference {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-sessions".
*/
export interface PayloadSession {
id: string;
session: string;
expiration: string;
user:
| {
relationTo: 'users';
value: string | User;
}
| {
relationTo: 'public-users';
value: string | PublicUser;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
@@ -939,6 +938,13 @@ export interface UsersSelect<T extends boolean = true> {
hash?: T;
loginAttempts?: T;
lockUntil?: T;
sessions?:
| T
| {
id?: T;
createdAt?: T;
expiresAt?: T;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -954,6 +960,13 @@ export interface PublicUsersSelect<T extends boolean = true> {
hash?: T;
loginAttempts?: T;
lockUntil?: T;
sessions?:
| T
| {
id?: T;
createdAt?: T;
expiresAt?: T;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -1294,6 +1307,13 @@ export interface AuthCollectionSelect<T extends boolean = true> {
_verificationToken?: T;
loginAttempts?: T;
lockUntil?: T;
sessions?:
| T
| {
id?: T;
createdAt?: T;
expiresAt?: T;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
@@ -1317,17 +1337,6 @@ export interface PayloadPreferencesSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-sessions_select".
*/
export interface PayloadSessionsSelect<T extends boolean = true> {
session?: T;
expiration?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".

View File

@@ -18,6 +18,7 @@ export const UploadCollection: CollectionConfig = {
height: 100,
},
],
adminThumbnail: () => 'https://payloadcms.com/images/universal-truth.jpg',
adminThumbnail: () =>
'https://raw.githubusercontent.com/payloadcms/website/refs/heads/main/public/images/universal-truth.jpg',
},
}

View File

@@ -363,6 +363,43 @@ describe('Document View', () => {
})
describe('drawers', () => {
test('document drawers do not unmount across save events', async () => {
// Navigate to a post document
await navigateToDoc(page, postsUrl)
// Open the relationship drawer
await page
.locator('.field-type.relationship .relationship--single-value__drawer-toggler')
.click()
const drawer = page.locator('[id^=doc-drawer_posts_1_]')
const drawerEditView = drawer.locator('.drawer__content .collection-edit')
await expect(drawerEditView).toBeVisible()
const drawerTitleField = drawerEditView.locator('#field-title')
const testTitle = 'Test Title for Persistence'
await drawerTitleField.fill(testTitle)
await expect(drawerTitleField).toHaveValue(testTitle)
await drawerEditView.evaluate((el) => {
el.setAttribute('data-test-instance', 'This is a test')
})
await expect(drawerEditView).toHaveAttribute('data-test-instance', 'This is a test')
await saveDocAndAssert(page, '[id^=doc-drawer_posts_1_] .drawer__content #action-save')
await expect(drawerEditView).toBeVisible()
await expect(drawerTitleField).toHaveValue(testTitle)
// Verify the element instance hasn't changed (i.e., it wasn't re-mounted and discarded the custom attribute)
await expect
.poll(async () => {
return await drawerEditView.getAttribute('data-test-instance')
})
.toBe('This is a test')
})
test('document drawers are visually stacking', async () => {
await navigateToDoc(page, postsUrl)
await page.locator('#field-title').fill(title)

View File

@@ -293,6 +293,13 @@ export interface User {
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
sessions?:
| {
id: string;
createdAt?: string | null;
expiresAt: string;
}[]
| null;
password?: string | null;
}
/**
@@ -820,6 +827,13 @@ export interface UsersSelect<T extends boolean = true> {
hash?: T;
loginAttempts?: T;
lockUntil?: T;
sessions?:
| T
| {
id?: T;
createdAt?: T;
expiresAt?: T;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema

View File

@@ -46,6 +46,16 @@ export default buildConfigWithDefaults({
},
],
},
{
slug: 'categories-custom-id',
versions: { drafts: true },
fields: [
{
type: 'number',
name: 'id',
},
],
},
{
slug: postsSlug,
fields: [
@@ -60,6 +70,11 @@ export default buildConfigWithDefaults({
relationTo: 'categories',
name: 'category',
},
{
type: 'relationship',
relationTo: 'categories-custom-id',
name: 'categoryCustomID',
},
{
name: 'localized',
type: 'text',
@@ -516,6 +531,11 @@ export default buildConfigWithDefaults({
type: 'json',
virtual: 'post.category.id',
},
{
name: 'postCategoryCustomID',
type: 'number',
virtual: 'post.categoryCustomID.id',
},
{
name: 'postID',
type: 'json',

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