Compare commits
57 Commits
feat/pushj
...
v3.53.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4151a902f2 | ||
|
|
b65ca6832d | ||
|
|
76741eb722 | ||
|
|
2bdd669fde | ||
|
|
96074530b1 | ||
|
|
5cf215d9cb | ||
|
|
393b4a0929 | ||
|
|
a94cd95b90 | ||
|
|
a04bc9a3e7 | ||
|
|
36fd6e905a | ||
|
|
c67ceca8e2 | ||
|
|
f382c39dae | ||
|
|
fea6742ceb | ||
|
|
aa90271a59 | ||
|
|
5e433aa9c3 | ||
|
|
c7b9f0f563 | ||
|
|
b3e48f8efa | ||
|
|
f44e27691e | ||
|
|
a840fc944b | ||
|
|
cf427e5519 | ||
|
|
adb83b1e06 | ||
|
|
368cd901f8 | ||
|
|
406a09f4bf | ||
|
|
4f6d0d8ed2 | ||
|
|
9e7bb24ffb | ||
|
|
73ba4d1bb9 | ||
|
|
332b2a9d3c | ||
|
|
92d459ec99 | ||
|
|
7699d02d7f | ||
|
|
b714e6b151 | ||
|
|
379ef87d84 | ||
|
|
9f7d8c65d5 | ||
|
|
30ea8e1bac | ||
|
|
f9bbca8bfe | ||
|
|
9d08f503ae | ||
|
|
a7ed88b5fa | ||
|
|
ec5b673aca | ||
|
|
3dd142c637 | ||
|
|
1909063e42 | ||
|
|
64f4b0aff3 | ||
|
|
c8ef92449b | ||
|
|
b7243b1413 | ||
|
|
f5d77662b0 | ||
|
|
efdf00200a | ||
|
|
217606ac20 | ||
|
|
0b60bf2eff | ||
|
|
46699ec314 | ||
|
|
cdd90f91c8 | ||
|
|
8d4e7f5f30 | ||
|
|
b426052cab | ||
|
|
047519f47f | ||
|
|
c1c68fbb55 | ||
|
|
3e65111bc1 | ||
|
|
0e8a6c0162 | ||
|
|
0688050eb6 | ||
|
|
5a99d8c5f4 | ||
|
|
35ca98e70e |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -331,5 +331,7 @@ test/databaseAdapter.js
|
||||
test/.localstack
|
||||
test/google-cloud-storage
|
||||
test/azurestoragedata/
|
||||
/media-without-delete-access
|
||||
|
||||
|
||||
licenses.csv
|
||||
|
||||
@@ -33,7 +33,7 @@ export const Users: CollectionConfig = {
|
||||
}
|
||||
```
|
||||
|
||||

|
||||

|
||||
_Admin Panel screenshot depicting an Admins Collection with Auth enabled_
|
||||
|
||||
## Config Options
|
||||
|
||||
@@ -141,7 +141,7 @@ The following options are available:
|
||||
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
|
||||
| `components` | Swap in your own React components to be used within this Collection. [More details](#custom-components). |
|
||||
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). |
|
||||
| `pagination` | Set pagination-specific options for this Collection. [More details](#pagination). |
|
||||
| `pagination` | Set pagination-specific options for this Collection in the List View. [More details](#pagination). |
|
||||
| `baseFilter` | Defines a default base filter which will be applied to the List View (along with any other filters applied by the user) and internal links in Lexical Editor, |
|
||||
|
||||
<Banner type="warning">
|
||||
|
||||
@@ -158,7 +158,7 @@ export function MyCustomView(props: AdminViewServerProps) {
|
||||
|
||||
<Banner type="success">
|
||||
**Tip:** For consistent layout and navigation, you may want to wrap your
|
||||
Custom View with one of the built-in [Template](./overview#templates).
|
||||
Custom View with one of the built-in [Templates](./overview#templates).
|
||||
</Banner>
|
||||
|
||||
### View Templates
|
||||
|
||||
@@ -293,7 +293,6 @@ Here's an example of a custom `editMenuItems` component:
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
import { PopupList } from '@payloadcms/ui'
|
||||
|
||||
import type { EditMenuItemsServerProps } from 'payload'
|
||||
|
||||
@@ -301,12 +300,12 @@ export const EditMenuItems = async (props: EditMenuItemsServerProps) => {
|
||||
const href = `/custom-action?id=${props.id}`
|
||||
|
||||
return (
|
||||
<PopupList.ButtonGroup>
|
||||
<PopupList.Button href={href}>Custom Edit Menu Item</PopupList.Button>
|
||||
<PopupList.Button href={href}>
|
||||
<>
|
||||
<a href={href}>Custom Edit Menu Item</a>
|
||||
<a href={href}>
|
||||
Another Custom Edit Menu Item - add as many as you need!
|
||||
</PopupList.Button>
|
||||
</PopupList.ButtonGroup>
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -63,3 +63,22 @@ export const MyCollection: CollectionConfig = {
|
||||
],
|
||||
}
|
||||
```
|
||||
## Localized fields and MongoDB indexes
|
||||
|
||||
When you set `index: true` or `unique: true` on a localized field, MongoDB creates one index **per locale path** (e.g., `slug.en`, `slug.da-dk`, etc.). With many locales and indexed fields, this can quickly approach MongoDB's per-collection index limit.
|
||||
|
||||
If you know you'll query specifically by a locale, index only those locale paths using the collection-level `indexes` option instead of setting `index: true` on the localized field. This approach gives you more control and helps avoid unnecessary indexes.
|
||||
|
||||
```ts
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
fields: [{ name: 'slug', type: 'text', localized: true }],
|
||||
indexes: [
|
||||
// Index English slug only (rather than all locales)
|
||||
{ fields: ['slug.en'] },
|
||||
// You could also make it unique:
|
||||
// { fields: ['slug.en'], unique: true },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
@@ -60,21 +60,21 @@ You can access Mongoose models as follows:
|
||||
|
||||
## Using other MongoDB implementations
|
||||
|
||||
You can import the `compatabilityOptions` object to get the recommended settings for other MongoDB implementations. Since these databases aren't officially supported by payload, you may still encounter issues even with these settings (please create an issue or PR if you believe these options should be updated):
|
||||
You can import the `compatibilityOptions` object to get the recommended settings for other MongoDB implementations. Since these databases aren't officially supported by payload, you may still encounter issues even with these settings (please create an issue or PR if you believe these options should be updated):
|
||||
|
||||
```ts
|
||||
import { mongooseAdapter, compatabilityOptions } from '@payloadcms/db-mongodb'
|
||||
import { mongooseAdapter, compatibilityOptions } from '@payloadcms/db-mongodb'
|
||||
|
||||
export default buildConfig({
|
||||
db: mongooseAdapter({
|
||||
url: process.env.DATABASE_URI,
|
||||
// For example, if you're using firestore:
|
||||
...compatabilityOptions.firestore,
|
||||
...compatibilityOptions.firestore,
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
We export compatability options for [DocumentDB](https://aws.amazon.com/documentdb/), [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db) and [Firestore](https://cloud.google.com/firestore/mongodb-compatibility/docs/overview). Known limitations:
|
||||
We export compatibility options for [DocumentDB](https://aws.amazon.com/documentdb/), [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db) and [Firestore](https://cloud.google.com/firestore/mongodb-compatibility/docs/overview). Known limitations:
|
||||
|
||||
- Azure Cosmos DB does not support transactions that update two or more documents in different collections, which is a common case when using Payload (via hooks).
|
||||
- Azure Cosmos DB the root config property `indexSortableFields` must be set to `true`.
|
||||
|
||||
@@ -162,6 +162,11 @@ const result = await payload.find({
|
||||
})
|
||||
```
|
||||
|
||||
<Banner type="info">
|
||||
`pagination`, `page`, and `limit` are three related properties [documented
|
||||
here](/docs/queries/pagination).
|
||||
</Banner>
|
||||
|
||||
### Find by ID#collection-find-by-id
|
||||
|
||||
```js
|
||||
|
||||
@@ -207,7 +207,7 @@ Everything mentioned above applies to local development as well, but there are a
|
||||
### Enable Turbopack
|
||||
|
||||
<Banner type="warning">
|
||||
**Note:** In the future this will be the default. Use as your own risk.
|
||||
**Note:** In the future this will be the default. Use at your own risk.
|
||||
</Banner>
|
||||
|
||||
Add `--turbo` to your dev script to significantly speed up your local development server start time.
|
||||
|
||||
@@ -85,8 +85,8 @@ type MultiTenantPluginConfig<ConfigTypes = unknown> = {
|
||||
*/
|
||||
tenantFieldOverrides?: CollectionTenantFieldConfigOverrides
|
||||
/**
|
||||
* Set to `false` if you want to manually apply the baseListFilter
|
||||
* Set to `false` if you want to manually apply the baseFilter
|
||||
* Set to `false` if you want to manually apply
|
||||
* the baseFilter
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
|
||||
@@ -13,8 +13,8 @@ keywords: uploads, images, media, overview, documentation, Content Management Sy
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/upload-admin.jpg"
|
||||
srcDark="https://payloadcms.com/images/docs/upload-admin.jpg"
|
||||
srcLight="https://payloadcms.com/images/docs/uploads-overview.jpg"
|
||||
srcDark="https://payloadcms.com/images/docs/uploads-overview.jpg"
|
||||
alt="Shows an Upload enabled collection in the Payload Admin Panel"
|
||||
caption="Admin Panel screenshot depicting a Media Collection with Upload enabled"
|
||||
/>
|
||||
|
||||
@@ -12,7 +12,7 @@ Extending on Payload's [Draft](/docs/versions/drafts) functionality, you can con
|
||||
Autosave relies on Versions and Drafts being enabled in order to function.
|
||||
</Banner>
|
||||
|
||||

|
||||

|
||||
_If Autosave is enabled, drafts will be created automatically as the document is modified and the Admin UI adds an indicator describing when the document was last saved to the top right of the sidebar._
|
||||
|
||||
## Options
|
||||
|
||||
@@ -14,7 +14,7 @@ Payload's Draft functionality builds on top of the Versions functionality to all
|
||||
|
||||
By enabling Versions with Drafts, your collections and globals can maintain _newer_, and _unpublished_ versions of your documents. It's perfect for cases where you might want to work on a document, update it and save your progress, but not necessarily make it publicly published right away. Drafts are extremely helpful when building preview implementations.
|
||||
|
||||

|
||||

|
||||
_If Drafts are enabled, the typical Save button is replaced with new actions which allow you to either save a draft, or publish your changes._
|
||||
|
||||
## Options
|
||||
|
||||
@@ -13,7 +13,7 @@ keywords: version history, revisions, audit log, draft, publish, restore, autosa
|
||||
|
||||
When enabled, Payload will automatically scaffold a new Collection in your database to store versions of your document(s) over time, and the Admin UI will be extended with additional views that allow you to browse document versions, view diffs in order to see exactly what has changed in your documents (and when they changed), and restore documents back to prior versions easily.
|
||||
|
||||

|
||||

|
||||
_Comparing an old version to a newer version of a document_
|
||||
|
||||
**With Versions, you can:**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/admin-bar",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "An admin bar for React apps using Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -331,7 +331,7 @@ export function mongooseAdapter({
|
||||
}
|
||||
}
|
||||
|
||||
export { compatabilityOptions } from './utilities/compatabilityOptions.js'
|
||||
export { compatibilityOptions } from './utilities/compatibilityOptions.js'
|
||||
|
||||
/**
|
||||
* Attempt to find migrations directory.
|
||||
|
||||
@@ -50,18 +50,12 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
|
||||
let result
|
||||
|
||||
let updateData: UpdateQuery<any> = data
|
||||
|
||||
const $inc: Record<string, number> = {}
|
||||
const $push: Record<string, { $each: any[] } | any> = {}
|
||||
|
||||
transform({ $inc, $push, adapter: this, data, fields, operation: 'write' })
|
||||
let updateData: UpdateQuery<any> = data
|
||||
transform({ $inc, adapter: this, data, fields, operation: 'write' })
|
||||
if (Object.keys($inc).length) {
|
||||
updateData = { $inc, $set: updateData }
|
||||
}
|
||||
if (Object.keys($push).length) {
|
||||
updateData = { $push, $set: updateData }
|
||||
}
|
||||
|
||||
try {
|
||||
if (returning === false) {
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { Args } from '../index.js'
|
||||
|
||||
/**
|
||||
* Each key is a mongo-compatible database and the value
|
||||
* is the recommended `mongooseAdapter` settings for compatability.
|
||||
* is the recommended `mongooseAdapter` settings for compatibility.
|
||||
*/
|
||||
export const compatabilityOptions = {
|
||||
export const compatibilityOptions = {
|
||||
cosmosdb: {
|
||||
transactionOptions: false,
|
||||
useJoinAggregations: false,
|
||||
@@ -12,6 +12,7 @@ export const compatabilityOptions = {
|
||||
},
|
||||
documentdb: {
|
||||
disableIndexHints: true,
|
||||
useJoinAggregations: false,
|
||||
},
|
||||
firestore: {
|
||||
disableIndexHints: true,
|
||||
@@ -209,7 +209,6 @@ const sanitizeDate = ({
|
||||
|
||||
type Args = {
|
||||
$inc?: Record<string, number>
|
||||
$push?: Record<string, { $each: any[] } | any>
|
||||
/** instance of the adapter */
|
||||
adapter: MongooseAdapter
|
||||
/** data to transform, can be an array of documents or a single document */
|
||||
@@ -399,7 +398,6 @@ const stripFields = ({
|
||||
|
||||
export const transform = ({
|
||||
$inc,
|
||||
$push,
|
||||
adapter,
|
||||
data,
|
||||
fields,
|
||||
@@ -414,16 +412,7 @@ export const transform = ({
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
for (const item of data) {
|
||||
transform({
|
||||
$inc,
|
||||
$push,
|
||||
adapter,
|
||||
data: item,
|
||||
fields,
|
||||
globalSlug,
|
||||
operation,
|
||||
validateRelationships,
|
||||
})
|
||||
transform({ $inc, adapter, data: item, fields, globalSlug, operation, validateRelationships })
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -481,26 +470,6 @@ export const transform = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
$push &&
|
||||
field.type === 'array' &&
|
||||
operation === 'write' &&
|
||||
field.name in ref &&
|
||||
ref[field.name]
|
||||
) {
|
||||
const value = ref[field.name]
|
||||
if (value && typeof value === 'object' && '$push' in value) {
|
||||
const push = value.$push
|
||||
|
||||
if (Array.isArray(push)) {
|
||||
$push[`${parentPath}${field.name}`] = { $each: push }
|
||||
} else if (typeof push === 'object') {
|
||||
$push[`${parentPath}${field.name}`] = push
|
||||
}
|
||||
delete ref[field.name]
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'date' && operation === 'read' && field.name in ref && ref[field.name]) {
|
||||
if (config.localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
|
||||
const fieldRef = ref[field.name] as Record<string, unknown>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -791,9 +791,14 @@ export const traverseFields = ({
|
||||
} else {
|
||||
shouldSelect = true
|
||||
}
|
||||
const tableName = fieldShouldBeLocalized({ field, parentIsLocalized })
|
||||
? `${currentTableName}${adapter.localesSuffix}`
|
||||
: currentTableName
|
||||
|
||||
if (shouldSelect) {
|
||||
args.extras[name] = sql.raw(`ST_AsGeoJSON(${toSnakeCase(name)})::jsonb`).as(name)
|
||||
args.extras[name] = sql
|
||||
.raw(`ST_AsGeoJSON("${adapter.tables[tableName][name].name}")::jsonb`)
|
||||
.as(name)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@ export const transformArray = ({
|
||||
data.forEach((arrayRow, i) => {
|
||||
const newRow: ArrayRowToInsert = {
|
||||
arrays: {},
|
||||
arraysToPush: {},
|
||||
locales: {},
|
||||
row: {
|
||||
_order: i + 1,
|
||||
@@ -105,7 +104,6 @@ export const transformArray = ({
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays: newRow.arrays,
|
||||
arraysToPush: newRow.arraysToPush,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
|
||||
@@ -78,7 +78,6 @@ export const transformBlocks = ({
|
||||
|
||||
const newRow: BlockRowToInsert = {
|
||||
arrays: {},
|
||||
arraysToPush: {},
|
||||
locales: {},
|
||||
row: {
|
||||
_order: i + 1,
|
||||
@@ -117,7 +116,6 @@ export const transformBlocks = ({
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays: newRow.arrays,
|
||||
arraysToPush: newRow.arraysToPush,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
|
||||
@@ -27,7 +27,6 @@ export const transformForWrite = ({
|
||||
// Split out the incoming data into rows to insert / delete
|
||||
const rowToInsert: RowToInsert = {
|
||||
arrays: {},
|
||||
arraysToPush: {},
|
||||
blocks: {},
|
||||
blocksToDelete: new Set(),
|
||||
locales: {},
|
||||
@@ -46,7 +45,6 @@ export const transformForWrite = ({
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays: rowToInsert.arrays,
|
||||
arraysToPush: rowToInsert.arraysToPush,
|
||||
baseTableName: tableName,
|
||||
blocks: rowToInsert.blocks,
|
||||
blocksToDelete: rowToInsert.blocksToDelete,
|
||||
|
||||
@@ -4,7 +4,13 @@ import { fieldIsVirtual, fieldShouldBeLocalized } from 'payload/shared'
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from '../../types.js'
|
||||
import type { NumberToDelete, RelationshipToDelete, RowToInsert, TextToDelete } from './types.js'
|
||||
import type {
|
||||
ArrayRowToInsert,
|
||||
BlockRowToInsert,
|
||||
NumberToDelete,
|
||||
RelationshipToDelete,
|
||||
TextToDelete,
|
||||
} from './types.js'
|
||||
|
||||
import { isArrayOfRows } from '../../utilities/isArrayOfRows.js'
|
||||
import { resolveBlockTableName } from '../../utilities/validateExistingBlockIsIdentical.js'
|
||||
@@ -17,20 +23,16 @@ import { transformTexts } from './texts.js'
|
||||
|
||||
type Args = {
|
||||
adapter: DrizzleAdapter
|
||||
/**
|
||||
* This will delete the array table and then re-insert all the new array rows.
|
||||
*/
|
||||
arrays: RowToInsert['arrays']
|
||||
/**
|
||||
* Array rows to push to the existing array. This will simply create
|
||||
* a new row in the array table.
|
||||
*/
|
||||
arraysToPush: RowToInsert['arraysToPush']
|
||||
arrays: {
|
||||
[tableName: string]: ArrayRowToInsert[]
|
||||
}
|
||||
/**
|
||||
* This is the name of the base table
|
||||
*/
|
||||
baseTableName: string
|
||||
blocks: RowToInsert['blocks']
|
||||
blocks: {
|
||||
[blockType: string]: BlockRowToInsert[]
|
||||
}
|
||||
blocksToDelete: Set<string>
|
||||
/**
|
||||
* A snake-case field prefix, representing prior fields
|
||||
@@ -80,7 +82,6 @@ type Args = {
|
||||
export const traverseFields = ({
|
||||
adapter,
|
||||
arrays,
|
||||
arraysToPush,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
@@ -128,6 +129,10 @@ export const traverseFields = ({
|
||||
if (field.type === 'array') {
|
||||
const arrayTableName = adapter.tableNameMap.get(`${parentTableName}_${columnName}`)
|
||||
|
||||
if (!arrays[arrayTableName]) {
|
||||
arrays[arrayTableName] = []
|
||||
}
|
||||
|
||||
if (isLocalized) {
|
||||
if (typeof data[field.name] === 'object' && data[field.name] !== null) {
|
||||
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
|
||||
@@ -152,33 +157,19 @@ export const traverseFields = ({
|
||||
textsToDelete,
|
||||
withinArrayOrBlockLocale: localeKey,
|
||||
})
|
||||
if (!arrays[arrayTableName]) {
|
||||
arrays[arrayTableName] = []
|
||||
}
|
||||
|
||||
arrays[arrayTableName] = arrays[arrayTableName].concat(newRows)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let value = data[field.name]
|
||||
let push = false
|
||||
if (
|
||||
// TODO do this for localized as well in DRY way
|
||||
|
||||
typeof value === 'object' &&
|
||||
'$push' in value
|
||||
) {
|
||||
value = Array.isArray(value.$push) ? value.$push : [value.$push]
|
||||
push = true
|
||||
}
|
||||
|
||||
const newRows = transformArray({
|
||||
adapter,
|
||||
arrayTableName,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
data: value,
|
||||
data: data[field.name],
|
||||
field,
|
||||
numbers,
|
||||
numbersToDelete,
|
||||
@@ -192,17 +183,7 @@ export const traverseFields = ({
|
||||
withinArrayOrBlockLocale,
|
||||
})
|
||||
|
||||
if (push) {
|
||||
if (!arraysToPush[arrayTableName]) {
|
||||
arraysToPush[arrayTableName] = []
|
||||
}
|
||||
arraysToPush[arrayTableName] = arraysToPush[arrayTableName].concat(newRows)
|
||||
} else {
|
||||
if (!arrays[arrayTableName]) {
|
||||
arrays[arrayTableName] = []
|
||||
}
|
||||
arrays[arrayTableName] = arrays[arrayTableName].concat(newRows)
|
||||
}
|
||||
arrays[arrayTableName] = arrays[arrayTableName].concat(newRows)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -283,7 +264,6 @@ export const traverseFields = ({
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays,
|
||||
arraysToPush,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
@@ -318,7 +298,6 @@ export const traverseFields = ({
|
||||
traverseFields({
|
||||
adapter,
|
||||
arrays,
|
||||
arraysToPush,
|
||||
baseTableName,
|
||||
blocks,
|
||||
blocksToDelete,
|
||||
|
||||
@@ -2,9 +2,6 @@ export type ArrayRowToInsert = {
|
||||
arrays: {
|
||||
[tableName: string]: ArrayRowToInsert[]
|
||||
}
|
||||
arraysToPush: {
|
||||
[tableName: string]: ArrayRowToInsert[]
|
||||
}
|
||||
locales: {
|
||||
[locale: string]: Record<string, unknown>
|
||||
}
|
||||
@@ -15,9 +12,6 @@ export type BlockRowToInsert = {
|
||||
arrays: {
|
||||
[tableName: string]: ArrayRowToInsert[]
|
||||
}
|
||||
arraysToPush: {
|
||||
[tableName: string]: ArrayRowToInsert[]
|
||||
}
|
||||
locales: {
|
||||
[locale: string]: Record<string, unknown>
|
||||
}
|
||||
@@ -43,9 +37,6 @@ export type RowToInsert = {
|
||||
arrays: {
|
||||
[tableName: string]: ArrayRowToInsert[]
|
||||
}
|
||||
arraysToPush: {
|
||||
[tableName: string]: ArrayRowToInsert[]
|
||||
}
|
||||
blocks: {
|
||||
[tableName: string]: BlockRowToInsert[]
|
||||
}
|
||||
|
||||
@@ -13,13 +13,9 @@ export const updateJobs: UpdateJobs = async function updateMany(
|
||||
this: DrizzleAdapter,
|
||||
{ id, data, limit: limitArg, req, returning, sort: sortArg, where: whereArg },
|
||||
) {
|
||||
if (
|
||||
!(data?.log as object[])?.length &&
|
||||
!(data.log && typeof data.log === 'object' && '$push' in data.log)
|
||||
) {
|
||||
if (!(data?.log as object[])?.length) {
|
||||
delete data.log
|
||||
}
|
||||
|
||||
const whereToUse: Where = id ? { id: { equals: id } } : whereArg
|
||||
const limit = id ? 1 : limitArg
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
}: Args): Promise<T> => {
|
||||
let insertedRow: Record<string, unknown> = { id }
|
||||
if (id && shouldUseOptimizedUpsertRow({ data, fields })) {
|
||||
const { arraysToPush, row } = transformForWrite({
|
||||
const { row } = transformForWrite({
|
||||
adapter,
|
||||
data,
|
||||
enableAtomicWrites: true,
|
||||
@@ -54,27 +54,11 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
|
||||
const drizzle = db as LibSQLDatabase
|
||||
|
||||
// First, handle $push arrays
|
||||
|
||||
if (arraysToPush && Object.keys(arraysToPush)?.length) {
|
||||
await insertArrays({
|
||||
adapter,
|
||||
arrays: [arraysToPush],
|
||||
db,
|
||||
parentRows: [insertedRow],
|
||||
uuidMap: {},
|
||||
})
|
||||
}
|
||||
|
||||
// Then, handle regular row update
|
||||
|
||||
if (ignoreResult) {
|
||||
if (row && Object.keys(row).length) {
|
||||
await drizzle
|
||||
.update(adapter.tables[tableName])
|
||||
.set(row)
|
||||
.where(eq(adapter.tables[tableName].id, id))
|
||||
}
|
||||
await drizzle
|
||||
.update(adapter.tables[tableName])
|
||||
.set(row)
|
||||
.where(eq(adapter.tables[tableName].id, id))
|
||||
return ignoreResult === 'idOnly' ? ({ id } as T) : null
|
||||
}
|
||||
|
||||
@@ -90,22 +74,6 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
const findManyKeysLength = Object.keys(findManyArgs).length
|
||||
const hasOnlyColumns = Object.keys(findManyArgs.columns || {}).length > 0
|
||||
|
||||
if (!row || !Object.keys(row).length) {
|
||||
// Nothing to update => just fetch current row and return
|
||||
findManyArgs.where = eq(adapter.tables[tableName].id, insertedRow.id)
|
||||
|
||||
const doc = await db.query[tableName].findFirst(findManyArgs)
|
||||
|
||||
return transform<T>({
|
||||
adapter,
|
||||
config: adapter.payload.config,
|
||||
data: doc,
|
||||
fields,
|
||||
joinQuery: false,
|
||||
tableName,
|
||||
})
|
||||
}
|
||||
|
||||
if (findManyKeysLength === 0 || hasOnlyColumns) {
|
||||
// Optimization - No need for joins => can simply use returning(). This is optimal for very simple collections
|
||||
// without complex fields that live in separate tables like blocks, arrays, relationships, etc.
|
||||
@@ -461,9 +429,9 @@ export const upsertRow = async <T extends Record<string, unknown> | TypeWithID>(
|
||||
|
||||
await insertArrays({
|
||||
adapter,
|
||||
arrays: [rowToInsert.arrays, rowToInsert.arraysToPush],
|
||||
arrays: [rowToInsert.arrays],
|
||||
db,
|
||||
parentRows: [insertedRow, insertedRow],
|
||||
parentRows: [insertedRow],
|
||||
uuidMap: arraysBlocksUUIDMap,
|
||||
})
|
||||
|
||||
|
||||
@@ -32,9 +32,6 @@ export const insertArrays = async ({
|
||||
const rowsByTable: RowsByTable = {}
|
||||
|
||||
arrays.forEach((arraysByTable, parentRowIndex) => {
|
||||
if (!arraysByTable || Object.keys(arraysByTable).length === 0) {
|
||||
return
|
||||
}
|
||||
Object.entries(arraysByTable).forEach(([tableName, arrayRows]) => {
|
||||
// If the table doesn't exist in map, initialize it
|
||||
if (!rowsByTable[tableName]) {
|
||||
|
||||
@@ -20,6 +20,7 @@ export const shouldUseOptimizedUpsertRow = ({
|
||||
}
|
||||
|
||||
if (
|
||||
field.type === 'array' ||
|
||||
field.type === 'blocks' ||
|
||||
((field.type === 'text' ||
|
||||
field.type === 'relationship' ||
|
||||
@@ -34,17 +35,6 @@ export const shouldUseOptimizedUpsertRow = ({
|
||||
return false
|
||||
}
|
||||
|
||||
if (field.type === 'array') {
|
||||
if (typeof value === 'object' && '$push' in value && value.$push) {
|
||||
return shouldUseOptimizedUpsertRow({
|
||||
// Only check first row - this function cares about field definitions. Each array row will have the same field definitions.
|
||||
data: Array.isArray(value.$push) ? value.$push?.[0] : value.$push,
|
||||
fields: field.flattenedFields,
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
(field.type === 'group' || field.type === 'tab') &&
|
||||
value &&
|
||||
|
||||
@@ -9,7 +9,12 @@ export const buildIndexName = ({
|
||||
name: string
|
||||
number?: number
|
||||
}): string => {
|
||||
const indexName = `${name}${number ? `_${number}` : ''}_idx`
|
||||
let indexName = `${name}${number ? `_${number}` : ''}_idx`
|
||||
|
||||
if (indexName.length > 60) {
|
||||
const suffix = `${number ? `_${number}` : ''}_idx`
|
||||
indexName = `${name.slice(0, 60 - suffix.length)}${suffix}`
|
||||
}
|
||||
|
||||
if (!adapter.indexes.has(indexName)) {
|
||||
adapter.indexes.add(indexName)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -333,6 +333,7 @@ export const renderDocument = async ({
|
||||
}
|
||||
|
||||
const documentSlots = renderDocumentSlots({
|
||||
id,
|
||||
collectionConfig,
|
||||
globalConfig,
|
||||
hasSavePermission,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type {
|
||||
BeforeDocumentControlsServerPropsOnly,
|
||||
DefaultServerFunctionArgs,
|
||||
DocumentSlots,
|
||||
EditMenuItemsServerPropsOnly,
|
||||
PayloadRequest,
|
||||
@@ -27,10 +26,11 @@ export const renderDocumentSlots: (args: {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
hasSavePermission: boolean
|
||||
id?: number | string
|
||||
permissions: SanitizedDocumentPermissions
|
||||
req: PayloadRequest
|
||||
}) => DocumentSlots = (args) => {
|
||||
const { collectionConfig, globalConfig, hasSavePermission, req } = args
|
||||
const { id, collectionConfig, globalConfig, hasSavePermission, req } = args
|
||||
|
||||
const components: DocumentSlots = {} as DocumentSlots
|
||||
|
||||
@@ -39,6 +39,7 @@ export const renderDocumentSlots: (args: {
|
||||
const isPreviewEnabled = collectionConfig?.admin?.preview || globalConfig?.admin?.preview
|
||||
|
||||
const serverProps: ServerProps = {
|
||||
id,
|
||||
i18n: req.i18n,
|
||||
payload: req.payload,
|
||||
user: req.user,
|
||||
@@ -169,10 +170,11 @@ export const renderDocumentSlots: (args: {
|
||||
return components
|
||||
}
|
||||
|
||||
export const renderDocumentSlotsHandler: ServerFunction<{ collectionSlug: string }> = async (
|
||||
args,
|
||||
) => {
|
||||
const { collectionSlug, req } = args
|
||||
export const renderDocumentSlotsHandler: ServerFunction<{
|
||||
collectionSlug: string
|
||||
id?: number | string
|
||||
}> = async (args) => {
|
||||
const { id, collectionSlug, req } = args
|
||||
|
||||
const collectionConfig = req.payload.collections[collectionSlug]?.config
|
||||
|
||||
@@ -187,6 +189,7 @@ export const renderDocumentSlotsHandler: ServerFunction<{ collectionSlug: string
|
||||
})
|
||||
|
||||
return renderDocumentSlots({
|
||||
id,
|
||||
collectionConfig,
|
||||
hasSavePermission,
|
||||
permissions: docPermissions,
|
||||
|
||||
@@ -26,9 +26,11 @@ export const LogoutClient: React.FC<{
|
||||
|
||||
const { startRouteTransition } = useRouteTransition()
|
||||
|
||||
const [isLoggedOut, setIsLoggedOut] = React.useState<boolean>(!user)
|
||||
const isLoggedIn = React.useMemo(() => {
|
||||
return Boolean(user?.id)
|
||||
}, [user?.id])
|
||||
|
||||
const logOutSuccessRef = React.useRef(false)
|
||||
const navigatingToLoginRef = React.useRef(false)
|
||||
|
||||
const [loginRoute] = React.useState(() =>
|
||||
formatAdminURL({
|
||||
@@ -45,26 +47,26 @@ export const LogoutClient: React.FC<{
|
||||
const router = useRouter()
|
||||
|
||||
const handleLogOut = React.useCallback(async () => {
|
||||
const loggedOut = await logOut()
|
||||
setIsLoggedOut(loggedOut)
|
||||
await logOut()
|
||||
|
||||
if (!inactivity && loggedOut && !logOutSuccessRef.current) {
|
||||
if (!inactivity && !navigatingToLoginRef.current) {
|
||||
toast.success(t('authentication:loggedOutSuccessfully'))
|
||||
logOutSuccessRef.current = true
|
||||
navigatingToLoginRef.current = true
|
||||
startRouteTransition(() => router.push(loginRoute))
|
||||
return
|
||||
}
|
||||
}, [inactivity, logOut, loginRoute, router, startRouteTransition, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedOut) {
|
||||
if (isLoggedIn) {
|
||||
void handleLogOut()
|
||||
} else {
|
||||
} else if (!navigatingToLoginRef.current) {
|
||||
navigatingToLoginRef.current = true
|
||||
startRouteTransition(() => router.push(loginRoute))
|
||||
}
|
||||
}, [handleLogOut, isLoggedOut, loginRoute, router, startRouteTransition])
|
||||
}, [handleLogOut, isLoggedIn, loginRoute, router, startRouteTransition])
|
||||
|
||||
if (isLoggedOut && inactivity) {
|
||||
if (!isLoggedIn && inactivity) {
|
||||
return (
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<h2>{t('authentication:loggedOutInactivity')}</h2>
|
||||
|
||||
@@ -90,7 +90,7 @@ export const SetStepNav: React.FC<{
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
label: t('version:versions'),
|
||||
url: formatAdminURL({
|
||||
adminRoute,
|
||||
path: `${docBasePath}/versions`,
|
||||
@@ -118,7 +118,7 @@ export const SetStepNav: React.FC<{
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
label: t('version:versions'),
|
||||
url: formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/globals/${globalSlug}/versions`,
|
||||
|
||||
@@ -20,13 +20,13 @@ import {
|
||||
import {
|
||||
fieldIsID,
|
||||
fieldShouldBeLocalized,
|
||||
getFieldPaths,
|
||||
getFieldPermissions,
|
||||
getUniqueListBy,
|
||||
tabHasName,
|
||||
} from 'payload/shared'
|
||||
|
||||
import { diffComponents } from './fields/index.js'
|
||||
import { getFieldPathsModified } from './utilities/getFieldPathsModified.js'
|
||||
|
||||
export type BuildVersionFieldsArgs = {
|
||||
clientSchemaMap: ClientFieldSchemaMap
|
||||
@@ -90,7 +90,7 @@ export const buildVersionFields = ({
|
||||
continue
|
||||
}
|
||||
|
||||
const { indexPath, path, schemaPath } = getFieldPathsModified({
|
||||
const { indexPath, path, schemaPath } = getFieldPaths({
|
||||
field,
|
||||
index: fieldIndex,
|
||||
parentIndexPath,
|
||||
@@ -286,7 +286,7 @@ const buildVersionField = ({
|
||||
indexPath: tabIndexPath,
|
||||
path: tabPath,
|
||||
schemaPath: tabSchemaPath,
|
||||
} = getFieldPathsModified({
|
||||
} = getFieldPaths({
|
||||
field: tabAsField,
|
||||
index: tabIndex,
|
||||
parentIndexPath: indexPath,
|
||||
@@ -322,14 +322,18 @@ const buildVersionField = ({
|
||||
nestingLevel: nestingLevel + 1,
|
||||
parentIndexPath: isNamedTab ? '' : tabIndexPath,
|
||||
parentIsLocalized: parentIsLocalized || tab.localized,
|
||||
parentPath: isNamedTab ? tabPath : path,
|
||||
parentSchemaPath: isNamedTab ? tabSchemaPath : parentSchemaPath,
|
||||
parentPath: isNamedTab ? tabPath : 'name' in field ? path : parentPath,
|
||||
parentSchemaPath: isNamedTab
|
||||
? tabSchemaPath
|
||||
: 'name' in field
|
||||
? schemaPath
|
||||
: parentSchemaPath,
|
||||
req,
|
||||
selectedLocales,
|
||||
versionFromSiblingData: 'name' in tab ? valueFrom?.[tab.name] : valueFrom,
|
||||
versionToSiblingData: 'name' in tab ? valueTo?.[tab.name] : valueTo,
|
||||
}).versionFields,
|
||||
label: tab.label,
|
||||
label: typeof tab.label === 'function' ? tab.label({ i18n, t: i18n.t }) : tab.label,
|
||||
}
|
||||
if (tabVersion?.fields?.length) {
|
||||
baseVersionField.tabs.push(tabVersion)
|
||||
@@ -370,8 +374,8 @@ const buildVersionField = ({
|
||||
nestingLevel: nestingLevel + 1,
|
||||
parentIndexPath: 'name' in field ? '' : indexPath,
|
||||
parentIsLocalized: parentIsLocalized || field.localized,
|
||||
parentPath: path + '.' + i,
|
||||
parentSchemaPath: schemaPath,
|
||||
parentPath: ('name' in field ? path : parentPath) + '.' + i,
|
||||
parentSchemaPath: 'name' in field ? schemaPath : parentSchemaPath,
|
||||
req,
|
||||
selectedLocales,
|
||||
versionFromSiblingData: fromRow,
|
||||
@@ -469,8 +473,8 @@ const buildVersionField = ({
|
||||
nestingLevel: nestingLevel + 1,
|
||||
parentIndexPath: 'name' in field ? '' : indexPath,
|
||||
parentIsLocalized: parentIsLocalized || ('localized' in field && field.localized),
|
||||
parentPath: path + '.' + i,
|
||||
parentSchemaPath: schemaPath + '.' + toBlock.slug,
|
||||
parentPath: ('name' in field ? path : parentPath) + '.' + i,
|
||||
parentSchemaPath: ('name' in field ? schemaPath : parentSchemaPath) + '.' + toBlock.slug,
|
||||
req,
|
||||
selectedLocales,
|
||||
versionFromSiblingData: fromRow,
|
||||
|
||||
@@ -25,7 +25,7 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
|
||||
parentIsLocalized,
|
||||
versionValue: valueTo,
|
||||
}) => {
|
||||
const { i18n } = useTranslation()
|
||||
const { i18n, t } = useTranslation()
|
||||
const { selectedLocales } = useSelectedLocales()
|
||||
const { config } = useConfig()
|
||||
|
||||
@@ -73,7 +73,9 @@ export const Iterable: React.FC<FieldDiffClientProps> = ({
|
||||
})
|
||||
|
||||
const rowNumber = String(i + 1).padStart(2, '0')
|
||||
const rowLabel = fieldIsArrayType(field) ? `Item ${rowNumber}` : `Block ${rowNumber}`
|
||||
const rowLabel = fieldIsArrayType(field)
|
||||
? `${t('general:item')} ${rowNumber}`
|
||||
: `${t('fields:block')} ${rowNumber}`
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__row`} key={i}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/payload-cloud",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
|
||||
@@ -25,7 +25,7 @@ type SelectFieldBaseClientProps = {
|
||||
readonly onChange?: (e: string | string[]) => void
|
||||
readonly path: string
|
||||
readonly validate?: SelectFieldValidation
|
||||
readonly value?: string
|
||||
readonly value?: string | string[]
|
||||
}
|
||||
|
||||
type SelectFieldBaseServerProps = Pick<FieldPaths, 'path'>
|
||||
|
||||
@@ -56,6 +56,12 @@ export type FieldState = {
|
||||
fieldSchema?: Field
|
||||
filterOptions?: FilterOptionsResult
|
||||
initialValue?: unknown
|
||||
/**
|
||||
* @experimental - Note: this property is experimental and may change in the future. Use at your own discretion.
|
||||
* Every time a field is changed locally, this flag is set to true. Prevents form state from server from overwriting local changes.
|
||||
* After merging server form state, this flag is reset.
|
||||
*/
|
||||
isModified?: boolean
|
||||
/**
|
||||
* The path of the field when its custom components were last rendered.
|
||||
* This is used to denote if a field has been rendered, and if so,
|
||||
@@ -114,9 +120,11 @@ export type BuildFormStateArgs = {
|
||||
mockRSCs?: boolean
|
||||
operation?: 'create' | 'update'
|
||||
readOnly?: boolean
|
||||
/*
|
||||
If true, will render field components within their state object
|
||||
*/
|
||||
/**
|
||||
* If true, will render field components within their state object.
|
||||
* Performance optimization: Setting to `false` ensures that only fields that have changed paths will re-render, e.g. new array rows, etc.
|
||||
* For example, you only need to render ALL fields on initial render, not on every onChange.
|
||||
*/
|
||||
renderAllFields?: boolean
|
||||
req: PayloadRequest
|
||||
returnLockStatus?: boolean
|
||||
|
||||
@@ -32,7 +32,7 @@ export type Options<TSlug extends CollectionSlug> = {
|
||||
locale?: TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -32,7 +32,7 @@ export type Options<TSlug extends CollectionSlug> = {
|
||||
locale?: TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -81,7 +81,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
|
||||
locale?: TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -46,7 +46,7 @@ export type BaseOptions<TSlug extends CollectionSlug, TSelect extends SelectType
|
||||
locale?: TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -62,7 +62,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
|
||||
locale?: TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -76,7 +76,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
|
||||
locale?: 'all' | TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -77,7 +77,7 @@ export type Options<
|
||||
locale?: 'all' | TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -54,7 +54,7 @@ export type Options<
|
||||
locale?: 'all' | TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-restricted-exports */
|
||||
import type { CollectionSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
|
||||
import type { Document, PayloadRequest, PopulateType, SelectType } from '../../../types/index.js'
|
||||
import type { CreateLocalReqOptions } from '../../../utilities/createLocalReq.js'
|
||||
@@ -48,7 +47,7 @@ export type Options<TSlug extends CollectionSlug> = {
|
||||
locale?: 'all' | TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-restricted-exports */
|
||||
import type { PaginatedDocs } from '../../../database/types.js'
|
||||
import type { CollectionSlug, Payload, RequestContext, TypedLocale } from '../../../index.js'
|
||||
import type {
|
||||
@@ -53,7 +52,7 @@ export type Options<TSlug extends CollectionSlug> = {
|
||||
locale?: 'all' | TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -41,7 +41,7 @@ export type Options<TSlug extends CollectionSlug> = {
|
||||
locale?: TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -76,7 +76,7 @@ export type BaseOptions<TSlug extends CollectionSlug, TSelect extends SelectType
|
||||
locale?: TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -402,6 +402,7 @@ export type Params = { [key: string]: string | string[] | undefined }
|
||||
export type ServerProps = {
|
||||
readonly documentSubViewType?: DocumentSubViewTypes
|
||||
readonly i18n: I18nClient
|
||||
readonly id?: number | string
|
||||
readonly locale?: Locale
|
||||
readonly params?: Params
|
||||
readonly payload: Payload
|
||||
|
||||
@@ -162,7 +162,12 @@ export async function validateSearchParam({
|
||||
if (versionFields) {
|
||||
fieldAccess = policies[entityType]![entitySlug]!.fields
|
||||
|
||||
if (segments[0] === 'parent' || segments[0] === 'version' || segments[0] === 'snapshot') {
|
||||
if (
|
||||
segments[0] === 'parent' ||
|
||||
segments[0] === 'version' ||
|
||||
segments[0] === 'snapshot' ||
|
||||
segments[0] === 'latest'
|
||||
) {
|
||||
segments.shift()
|
||||
}
|
||||
} else {
|
||||
|
||||
1
packages/payload/src/exports/i18n/is.ts
Normal file
1
packages/payload/src/exports/i18n/is.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { is } from '@payloadcms/translations/languages/is'
|
||||
@@ -2,8 +2,7 @@ import ObjectIdImport from 'bson-objectid'
|
||||
|
||||
import type { TextField } from '../config/types.js'
|
||||
|
||||
const ObjectId = (ObjectIdImport.default ||
|
||||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
|
||||
const ObjectId = 'default' in ObjectIdImport ? ObjectIdImport.default : ObjectIdImport
|
||||
|
||||
export const baseIDField: TextField = {
|
||||
name: 'id',
|
||||
|
||||
@@ -49,6 +49,9 @@ export function getFieldPaths({
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated - will be removed in 4.0. Use `getFieldPaths` instead.
|
||||
*/
|
||||
export function getFieldPathsModified({
|
||||
field,
|
||||
index,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Ajv from 'ajv'
|
||||
import ObjectIdImport from 'bson-objectid'
|
||||
|
||||
const ObjectId = (ObjectIdImport.default ||
|
||||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
|
||||
const ObjectId = 'default' in ObjectIdImport ? ObjectIdImport.default : ObjectIdImport
|
||||
|
||||
import type { TFunction } from '@payloadcms/translations'
|
||||
import type { JSONSchema4 } from 'json-schema'
|
||||
|
||||
@@ -51,6 +51,15 @@ export async function buildFolderWhereConstraints({
|
||||
equals: collectionConfig.slug,
|
||||
},
|
||||
})
|
||||
|
||||
// join queries need to omit trashed documents
|
||||
if (collectionConfig.trash) {
|
||||
constraints.push({
|
||||
deletedAt: {
|
||||
exists: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const filteredConstraints = constraints.filter(Boolean)
|
||||
|
||||
@@ -32,7 +32,7 @@ export type CountGlobalVersionsOptions<TSlug extends GlobalSlug> = {
|
||||
locale?: TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -43,7 +43,7 @@ export type Options<TSlug extends GlobalSlug, TSelect extends SelectType> = {
|
||||
locale?: 'all' | TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -39,7 +39,7 @@ export type Options<TSlug extends GlobalSlug> = {
|
||||
locale?: 'all' | TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -44,7 +44,7 @@ export type Options<TSlug extends GlobalSlug> = {
|
||||
locale?: 'all' | TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -33,7 +33,7 @@ export type Options<TSlug extends GlobalSlug> = {
|
||||
locale?: TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -51,7 +51,7 @@ export type Options<TSlug extends GlobalSlug, TSelect extends SelectType> = {
|
||||
locale?: 'all' | TypedLocale
|
||||
/**
|
||||
* Skip access control.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the fron-end.
|
||||
* Set to `false` if you want to respect Access Control for the operation, for example when fetching data for the front-end.
|
||||
* @default true
|
||||
*/
|
||||
overrideAccess?: boolean
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ObjectIdImport from 'bson-objectid'
|
||||
|
||||
import type { JobLog, PayloadRequest } from '../../index.js'
|
||||
import type { PayloadRequest } from '../../index.js'
|
||||
import type { RunJobsSilent } from '../localAPI.js'
|
||||
import type { UpdateJobFunction } from '../operations/runJobs/runJob/getUpdateJobFunction.js'
|
||||
import type { TaskError } from './index.js'
|
||||
@@ -9,8 +9,7 @@ import { getCurrentDate } from '../utilities/getCurrentDate.js'
|
||||
import { calculateBackoffWaitUntil } from './calculateBackoffWaitUntil.js'
|
||||
import { getWorkflowRetryBehavior } from './getWorkflowRetryBehavior.js'
|
||||
|
||||
const ObjectId = (ObjectIdImport.default ||
|
||||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
|
||||
const ObjectId = 'default' in ObjectIdImport ? ObjectIdImport.default : ObjectIdImport
|
||||
|
||||
export async function handleTaskError({
|
||||
error,
|
||||
@@ -60,6 +59,19 @@ export async function handleTaskError({
|
||||
|
||||
const currentDate = getCurrentDate()
|
||||
|
||||
;(job.log ??= []).push({
|
||||
id: new ObjectId().toHexString(),
|
||||
completedAt: currentDate.toISOString(),
|
||||
error: errorJSON,
|
||||
executedAt: executedAt.toISOString(),
|
||||
input,
|
||||
output: output ?? {},
|
||||
parent: req.payload.config.jobs.addParentToTaskLog ? parent : undefined,
|
||||
state: 'failed',
|
||||
taskID,
|
||||
taskSlug,
|
||||
})
|
||||
|
||||
if (job.waitUntil) {
|
||||
// Check if waitUntil is in the past
|
||||
const waitUntil = new Date(job.waitUntil)
|
||||
@@ -87,19 +99,6 @@ export async function handleTaskError({
|
||||
maxRetries = retriesConfig.attempts
|
||||
}
|
||||
|
||||
const taskLogToPush: JobLog = {
|
||||
id: new ObjectId().toHexString(),
|
||||
completedAt: currentDate.toISOString(),
|
||||
error: errorJSON,
|
||||
executedAt: executedAt.toISOString(),
|
||||
input,
|
||||
output: output ?? {},
|
||||
parent: req.payload.config.jobs.addParentToTaskLog ? parent : undefined,
|
||||
state: 'failed',
|
||||
taskID,
|
||||
taskSlug,
|
||||
}
|
||||
|
||||
if (!taskStatus?.complete && (taskStatus?.totalTried ?? 0) >= maxRetries) {
|
||||
/**
|
||||
* Task reached max retries => workflow will not retry
|
||||
@@ -108,9 +107,7 @@ export async function handleTaskError({
|
||||
await updateJob({
|
||||
error: errorJSON,
|
||||
hasError: true,
|
||||
log: {
|
||||
$push: taskLogToPush,
|
||||
} as any,
|
||||
log: job.log,
|
||||
processing: false,
|
||||
totalTried: (job.totalTried ?? 0) + 1,
|
||||
waitUntil: job.waitUntil,
|
||||
@@ -170,9 +167,7 @@ export async function handleTaskError({
|
||||
await updateJob({
|
||||
error: hasFinalError ? errorJSON : undefined,
|
||||
hasError: hasFinalError, // If reached max retries => final error. If hasError is true this job will not be retried
|
||||
log: {
|
||||
$push: taskLogToPush,
|
||||
} as any,
|
||||
log: job.log,
|
||||
processing: false,
|
||||
totalTried: (job.totalTried ?? 0) + 1,
|
||||
waitUntil: job.waitUntil,
|
||||
|
||||
@@ -79,6 +79,7 @@ export async function handleWorkflowError({
|
||||
await updateJob({
|
||||
error: errorJSON,
|
||||
hasError: hasFinalError, // If reached max retries => final error. If hasError is true this job will not be retried
|
||||
log: job.log,
|
||||
processing: false,
|
||||
totalTried: (job.totalTried ?? 0) + 1,
|
||||
waitUntil: job.waitUntil,
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
TaskType,
|
||||
} from '../../../config/types/taskTypes.js'
|
||||
import type {
|
||||
JobLog,
|
||||
SingleTaskStatus,
|
||||
WorkflowConfig,
|
||||
WorkflowTypes,
|
||||
@@ -24,8 +23,7 @@ import { TaskError } from '../../../errors/index.js'
|
||||
import { getCurrentDate } from '../../../utilities/getCurrentDate.js'
|
||||
import { getTaskHandlerFromConfig } from './importHandlerPath.js'
|
||||
|
||||
const ObjectId = (ObjectIdImport.default ||
|
||||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
|
||||
const ObjectId = 'default' in ObjectIdImport ? ObjectIdImport.default : ObjectIdImport
|
||||
|
||||
export type TaskParent = {
|
||||
taskID: string
|
||||
@@ -186,7 +184,7 @@ export const getRunTaskFunction = <TIsInline extends boolean>(
|
||||
await taskConfig.onSuccess()
|
||||
}
|
||||
|
||||
const newLogItem: JobLog = {
|
||||
;(job.log ??= []).push({
|
||||
id: new ObjectId().toHexString(),
|
||||
completedAt: getCurrentDate().toISOString(),
|
||||
executedAt: executedAt.toISOString(),
|
||||
@@ -196,12 +194,10 @@ export const getRunTaskFunction = <TIsInline extends boolean>(
|
||||
state: 'succeeded',
|
||||
taskID,
|
||||
taskSlug,
|
||||
}
|
||||
})
|
||||
|
||||
await updateJob({
|
||||
log: {
|
||||
$push: newLogItem,
|
||||
} as any,
|
||||
log: job.log,
|
||||
})
|
||||
|
||||
return output
|
||||
|
||||
@@ -37,6 +37,33 @@ type Result<T> = Promise<{
|
||||
files: FileToSave[]
|
||||
}>
|
||||
|
||||
const shouldReupload = (
|
||||
uploadEdits: UploadEdits,
|
||||
fileData: Record<string, unknown> | undefined,
|
||||
) => {
|
||||
if (!fileData) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (uploadEdits.crop || uploadEdits.heightInPixels || uploadEdits.widthInPixels) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Since uploadEdits always has focalPoint, compare to the value in the data if it was changed
|
||||
if (uploadEdits.focalPoint) {
|
||||
const incomingFocalX = uploadEdits.focalPoint.x
|
||||
const incomingFocalY = uploadEdits.focalPoint.y
|
||||
|
||||
const currentFocalX = 'focalX' in fileData && fileData.focalX
|
||||
const currentFocalY = 'focalY' in fileData && fileData.focalY
|
||||
|
||||
const isEqual = incomingFocalX === currentFocalX && incomingFocalY === currentFocalY
|
||||
return !isEqual
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const generateFileData = async <T>({
|
||||
collection: { config: collectionConfig },
|
||||
data,
|
||||
@@ -67,7 +94,7 @@ export const generateFileData = async <T>({
|
||||
})
|
||||
|
||||
const {
|
||||
constructorOptions = {},
|
||||
constructorOptions,
|
||||
disableLocalStorage,
|
||||
focalPoint: focalPointEnabled = true,
|
||||
formatOptions,
|
||||
@@ -82,7 +109,10 @@ export const generateFileData = async <T>({
|
||||
|
||||
const incomingFileData = isDuplicating ? originalDoc : data
|
||||
|
||||
if (!file && uploadEdits && incomingFileData) {
|
||||
if (
|
||||
!file &&
|
||||
(isDuplicating || shouldReupload(uploadEdits, incomingFileData as Record<string, unknown>))
|
||||
) {
|
||||
const { filename, url } = incomingFileData as unknown as FileData
|
||||
|
||||
if (filename && (filename.includes('../') || filename.includes('..\\'))) {
|
||||
|
||||
@@ -13,22 +13,28 @@ type Args = {
|
||||
export const getExternalFile = async ({ data, req, uploadConfig }: Args): Promise<File> => {
|
||||
const { filename, url } = data
|
||||
|
||||
let trimAuthCookies = true
|
||||
if (typeof url === 'string') {
|
||||
let fileURL = url
|
||||
if (!url.startsWith('http')) {
|
||||
// URL points to the same server - we can send any cookies safely to our server.
|
||||
trimAuthCookies = false
|
||||
const baseUrl = req.headers.get('origin') || `${req.protocol}://${req.headers.get('host')}`
|
||||
fileURL = `${baseUrl}${url}`
|
||||
}
|
||||
|
||||
let cookies = (req.headers.get('cookie') ?? '').split(';')
|
||||
|
||||
if (trimAuthCookies) {
|
||||
cookies = cookies.filter(
|
||||
(cookie) => !cookie.trim().startsWith(req.payload.config.cookiePrefix),
|
||||
)
|
||||
}
|
||||
|
||||
const headers = uploadConfig.externalFileHeaderFilter
|
||||
? uploadConfig.externalFileHeaderFilter(Object.fromEntries(new Headers(req.headers)))
|
||||
: {
|
||||
cookie:
|
||||
req.headers
|
||||
.get('cookie')
|
||||
?.split(';')
|
||||
.filter((cookie) => !cookie.trim().startsWith(req.payload.config.cookiePrefix))
|
||||
.join(';') || '',
|
||||
cookie: cookies.join(';'),
|
||||
}
|
||||
|
||||
// Check if URL is allowed because of skipSafeFetch allowList
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ObjectIdImport from 'bson-objectid'
|
||||
|
||||
const ObjectId = (ObjectIdImport.default ||
|
||||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
|
||||
const ObjectId = 'default' in ObjectIdImport ? ObjectIdImport.default : ObjectIdImport
|
||||
|
||||
export const isValidID = (
|
||||
value: number | string,
|
||||
|
||||
@@ -416,20 +416,11 @@ export const traverseFields = ({
|
||||
})
|
||||
) {
|
||||
if (Array.isArray(currentRef)) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const key in currentRef as Record<string, unknown>) {
|
||||
const localeData = currentRef[key as keyof typeof currentRef]
|
||||
if (!Array.isArray(localeData)) {
|
||||
continue
|
||||
}
|
||||
|
||||
traverseArrayOrBlocksField({
|
||||
callback,
|
||||
callbackStack,
|
||||
config,
|
||||
data: localeData,
|
||||
data: currentRef,
|
||||
field,
|
||||
fillEmpty,
|
||||
leavesFirst,
|
||||
@@ -437,6 +428,26 @@ export const traverseFields = ({
|
||||
parentPath,
|
||||
parentRef: currentParentRef,
|
||||
})
|
||||
} else {
|
||||
for (const key in currentRef as Record<string, unknown>) {
|
||||
const localeData = currentRef[key as keyof typeof currentRef]
|
||||
if (!Array.isArray(localeData)) {
|
||||
continue
|
||||
}
|
||||
|
||||
traverseArrayOrBlocksField({
|
||||
callback,
|
||||
callbackStack,
|
||||
config,
|
||||
data: localeData,
|
||||
field,
|
||||
fillEmpty,
|
||||
leavesFirst,
|
||||
parentIsLocalized: true,
|
||||
parentPath,
|
||||
parentRef: currentParentRef,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(currentRef)) {
|
||||
traverseArrayOrBlocksField({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -20,6 +20,7 @@ export type {
|
||||
PaymentField,
|
||||
PaymentFieldConfig,
|
||||
PriceCondition,
|
||||
RadioField,
|
||||
Redirect,
|
||||
SelectField,
|
||||
SelectFieldOption,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-import-export",
|
||||
"version": "3.50.0",
|
||||
"version": "3.53.0",
|
||||
"description": "Import-Export plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -73,7 +73,17 @@ export const ExportSaveButton: React.FC = () => {
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to download file')
|
||||
// Try to parse the error message from the JSON response
|
||||
let errorMsg = 'Failed to download file'
|
||||
try {
|
||||
const errorJson = await response.json()
|
||||
if (errorJson?.errors?.[0]?.message) {
|
||||
errorMsg = errorJson.errors[0].message
|
||||
}
|
||||
} catch {
|
||||
// Ignore JSON parse errors, fallback to generic message
|
||||
}
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
|
||||
const fileStream = response.body
|
||||
@@ -98,9 +108,8 @@ export const ExportSaveButton: React.FC = () => {
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error)
|
||||
toast.error('Error downloading file')
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Error downloading file')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.page-field {
|
||||
--field-width: 33.3333%;
|
||||
}
|
||||
41
packages/plugin-import-export/src/components/Page/index.tsx
Normal file
41
packages/plugin-import-export/src/components/Page/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import type { NumberFieldClientComponent } from 'payload'
|
||||
|
||||
import { NumberField, useField } from '@payloadcms/ui'
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'page-field'
|
||||
|
||||
export const Page: NumberFieldClientComponent = (props) => {
|
||||
const { setValue } = useField<number>()
|
||||
const { value: limitValue } = useField<number>({ path: 'limit' })
|
||||
|
||||
// Effect to reset page to 1 if limit is removed
|
||||
useEffect(() => {
|
||||
if (!limitValue) {
|
||||
setValue(1) // Reset page to 1
|
||||
}
|
||||
}, [limitValue, setValue])
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<NumberField
|
||||
field={{
|
||||
name: props.field.name,
|
||||
admin: {
|
||||
autoComplete: undefined,
|
||||
placeholder: undefined,
|
||||
step: 1,
|
||||
},
|
||||
label: props.field.label,
|
||||
min: 1,
|
||||
}}
|
||||
onChange={(value) => setValue(value ?? 1)} // Update the page value on change
|
||||
path={props.path}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export const Preview = () => {
|
||||
const { collection } = useImportExport()
|
||||
const { config } = useConfig()
|
||||
const { value: where } = useField({ path: 'where' })
|
||||
const { value: page } = useField({ path: 'page' })
|
||||
const { value: limit } = useField<number>({ path: 'limit' })
|
||||
const { value: fields } = useField<string[]>({ path: 'fields' })
|
||||
const { value: sort } = useField({ path: 'sort' })
|
||||
@@ -71,6 +72,7 @@ export const Preview = () => {
|
||||
format,
|
||||
limit,
|
||||
locale,
|
||||
page,
|
||||
sort,
|
||||
where,
|
||||
}),
|
||||
@@ -168,6 +170,7 @@ export const Preview = () => {
|
||||
i18n,
|
||||
limit,
|
||||
locale,
|
||||
page,
|
||||
sort,
|
||||
where,
|
||||
])
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
.sort-by-fields {
|
||||
display: block;
|
||||
width: 33%;
|
||||
--field-width: 25%;
|
||||
}
|
||||
|
||||
@@ -11,21 +11,32 @@ import {
|
||||
useField,
|
||||
useListQuery,
|
||||
} from '@payloadcms/ui'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { applySortOrder, normalizeQueryParam, stripSortDash } from '../../utilities/sortHelpers.js'
|
||||
import { reduceFields } from '../FieldsToExport/reduceFields.js'
|
||||
import { useImportExport } from '../ImportExportProvider/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'sort-by-fields'
|
||||
|
||||
export const SortBy: SelectFieldClientComponent = (props) => {
|
||||
const { id } = useDocumentInfo()
|
||||
const { setValue, value } = useField<string>()
|
||||
|
||||
// The "sort" text field that stores 'title' or '-title'
|
||||
const { setValue: setSort, value: sortRaw } = useField<string>()
|
||||
|
||||
// Sibling order field ('asc' | 'desc') used when writing sort on change
|
||||
const { value: sortOrder = 'asc' } = useField<string>({ path: 'sortOrder' })
|
||||
// Needed so we can initialize sortOrder when SortOrder component is hidden
|
||||
const { setValue: setSortOrder } = useField<'asc' | 'desc'>({ path: 'sortOrder' })
|
||||
|
||||
const { value: collectionSlug } = useField<string>({ path: 'collectionSlug' })
|
||||
const { query } = useListQuery()
|
||||
const { getEntityConfig } = useConfig()
|
||||
const { collection } = useImportExport()
|
||||
|
||||
// ReactSelect's displayed option
|
||||
const [displayedValue, setDisplayedValue] = useState<{
|
||||
id: string
|
||||
label: ReactNode
|
||||
@@ -33,45 +44,83 @@ export const SortBy: SelectFieldClientComponent = (props) => {
|
||||
} | null>(null)
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug: collectionSlug ?? collection })
|
||||
const fieldOptions = reduceFields({ fields: collectionConfig?.fields })
|
||||
const fieldOptions = useMemo(
|
||||
() => reduceFields({ fields: collectionConfig?.fields }),
|
||||
[collectionConfig?.fields],
|
||||
)
|
||||
|
||||
// Sync displayedValue with value from useField
|
||||
// Normalize the stored value for display (strip the '-') and pick the option
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
const clean = stripSortDash(sortRaw)
|
||||
if (!clean) {
|
||||
setDisplayedValue(null)
|
||||
return
|
||||
}
|
||||
|
||||
const option = fieldOptions.find((field) => field.value === value)
|
||||
if (option && (!displayedValue || displayedValue.value !== value)) {
|
||||
const option = fieldOptions.find((f) => f.value === clean)
|
||||
if (option && (!displayedValue || displayedValue.value !== clean)) {
|
||||
setDisplayedValue(option)
|
||||
}
|
||||
}, [displayedValue, fieldOptions, value])
|
||||
}, [sortRaw, fieldOptions, displayedValue])
|
||||
|
||||
// One-time init guard so clearing `sort` doesn't rehydrate from query again
|
||||
const didInitRef = useRef(false)
|
||||
|
||||
// Sync the visible select from list-view query sort (preferred) or groupBy (fallback)
|
||||
// and initialize both `sort` and `sortOrder` here as SortOrder may be hidden by admin.condition.
|
||||
useEffect(() => {
|
||||
if (id || !query?.sort || value) {
|
||||
if (didInitRef.current) {
|
||||
return
|
||||
}
|
||||
if (id) {
|
||||
didInitRef.current = true
|
||||
return
|
||||
}
|
||||
if (typeof sortRaw === 'string' && sortRaw.length > 0) {
|
||||
// Already initialized elsewhere
|
||||
didInitRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
const option = fieldOptions.find((field) => field.value === query.sort)
|
||||
const qsSort = normalizeQueryParam(query?.sort)
|
||||
const qsGroupBy = normalizeQueryParam(query?.groupBy)
|
||||
|
||||
const source = qsSort ?? qsGroupBy
|
||||
if (!source) {
|
||||
didInitRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
const isDesc = !!qsSort && qsSort.startsWith('-')
|
||||
const base = stripSortDash(source)
|
||||
const order: 'asc' | 'desc' = isDesc ? 'desc' : 'asc'
|
||||
|
||||
// Write BOTH fields so preview/export have the right values even if SortOrder is hidden
|
||||
setSort(applySortOrder(base, order))
|
||||
setSortOrder(order)
|
||||
|
||||
const option = fieldOptions.find((f) => f.value === base)
|
||||
if (option) {
|
||||
setValue(option.value)
|
||||
setDisplayedValue(option)
|
||||
}
|
||||
}, [fieldOptions, id, query?.sort, value, setValue])
|
||||
|
||||
didInitRef.current = true
|
||||
}, [id, query?.groupBy, query?.sort, sortRaw, fieldOptions, setSort, setSortOrder])
|
||||
|
||||
// When user selects a different field, store it with the current order applied
|
||||
const onChange = (option: { id: string; label: ReactNode; value: string } | null) => {
|
||||
if (!option) {
|
||||
setValue('')
|
||||
setSort('')
|
||||
setDisplayedValue(null)
|
||||
} else {
|
||||
setValue(option.value)
|
||||
setDisplayedValue(option)
|
||||
const next = applySortOrder(option.value, String(sortOrder) as 'asc' | 'desc')
|
||||
setSort(next)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass} style={{ '--field-width': '33%' } as React.CSSProperties}>
|
||||
<div className={baseClass}>
|
||||
<FieldLabel label={props.field.label} path={props.path} />
|
||||
<ReactSelect
|
||||
className={baseClass}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.sort-order-field {
|
||||
--field-width: 25%;
|
||||
}
|
||||
126
packages/plugin-import-export/src/components/SortOrder/index.tsx
Normal file
126
packages/plugin-import-export/src/components/SortOrder/index.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
'use client'
|
||||
|
||||
import type { SelectFieldClientComponent } from 'payload'
|
||||
|
||||
import { FieldLabel, ReactSelect, useDocumentInfo, useField, useListQuery } from '@payloadcms/ui'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { applySortOrder, normalizeQueryParam, stripSortDash } from '../../utilities/sortHelpers.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'sort-order-field'
|
||||
|
||||
type Order = 'asc' | 'desc'
|
||||
type OrderOption = { label: string; value: Order }
|
||||
|
||||
const options = [
|
||||
{ label: 'Ascending', value: 'asc' as const },
|
||||
{ label: 'Descending', value: 'desc' as const },
|
||||
] as const
|
||||
|
||||
const defaultOption: OrderOption = options[0]
|
||||
|
||||
export const SortOrder: SelectFieldClientComponent = (props) => {
|
||||
const { id } = useDocumentInfo()
|
||||
const { query } = useListQuery()
|
||||
|
||||
// 'sortOrder' select field: 'asc' | 'desc'
|
||||
const { setValue: setOrder, value: orderValueRaw } = useField<Order>()
|
||||
|
||||
// 'sort' text field: 'title' | '-title'
|
||||
const { setValue: setSort, value: sortRaw } = useField<string>({ path: 'sort' })
|
||||
|
||||
// The current order value, defaulting to 'asc' for UI
|
||||
const orderValue: Order = orderValueRaw || 'asc'
|
||||
|
||||
// Map 'asc' | 'desc' to the option object for ReactSelect
|
||||
const currentOption = useMemo<OrderOption>(
|
||||
() => options.find((o) => o.value === orderValue) ?? defaultOption,
|
||||
[orderValue],
|
||||
)
|
||||
const [displayed, setDisplayed] = useState<null | OrderOption>(currentOption)
|
||||
|
||||
// One-time init guard so clearing `sort` doesn't rehydrate from query again
|
||||
const didInitRef = useRef(false)
|
||||
|
||||
// Derive from list-view query.sort if present; otherwise fall back to groupBy
|
||||
useEffect(() => {
|
||||
if (didInitRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
// Existing export -> don't initialize here
|
||||
if (id) {
|
||||
didInitRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
// If sort already has a value, treat as initialized
|
||||
if (typeof sortRaw === 'string' && sortRaw.length > 0) {
|
||||
didInitRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
const qsSort = normalizeQueryParam(query?.sort)
|
||||
const qsGroupBy = normalizeQueryParam(query?.groupBy)
|
||||
|
||||
if (qsSort) {
|
||||
const isDesc = qsSort.startsWith('-')
|
||||
const base = stripSortDash(qsSort)
|
||||
const order: Order = isDesc ? 'desc' : 'asc'
|
||||
setOrder(order)
|
||||
setSort(applySortOrder(base, order)) // combined: 'title' or '-title'
|
||||
didInitRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback: groupBy (always ascending)
|
||||
if (qsGroupBy) {
|
||||
setOrder('asc')
|
||||
setSort(applySortOrder(qsGroupBy, 'asc')) // write 'groupByField' (no dash)
|
||||
didInitRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
// Nothing to initialize
|
||||
didInitRef.current = true
|
||||
}, [id, query?.sort, query?.groupBy, sortRaw, setOrder, setSort])
|
||||
|
||||
// Keep the select's displayed option in sync with the stored order
|
||||
useEffect(() => {
|
||||
setDisplayed(currentOption ?? defaultOption)
|
||||
}, [currentOption])
|
||||
|
||||
// Handle manual order changes via ReactSelect:
|
||||
// - update the order field
|
||||
// - rewrite the combined "sort" string to add/remove the leading '-'
|
||||
const onChange = (option: null | OrderOption) => {
|
||||
const next = option?.value ?? 'asc'
|
||||
setOrder(next)
|
||||
|
||||
const base = stripSortDash(sortRaw)
|
||||
if (base) {
|
||||
setSort(applySortOrder(base, next))
|
||||
}
|
||||
|
||||
setDisplayed(option ?? defaultOption)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<FieldLabel label={props.field.label} path={props.path} />
|
||||
<ReactSelect
|
||||
className={baseClass}
|
||||
disabled={props.readOnly}
|
||||
inputId={`field-${props.path.replace(/\./g, '__')}`}
|
||||
isClearable={false}
|
||||
isSearchable={false}
|
||||
// @ts-expect-error react-select option typing differs from our local type
|
||||
onChange={onChange}
|
||||
options={options as unknown as OrderOption[]}
|
||||
// @ts-expect-error react-select option typing differs from our local type
|
||||
value={displayed}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { APIError } from 'payload'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
import { buildDisabledFieldRegex } from '../utilities/buildDisabledFieldRegex.js'
|
||||
import { validateLimitValue } from '../utilities/validateLimitValue.js'
|
||||
import { flattenObject } from './flattenObject.js'
|
||||
import { getCustomFieldFunctions } from './getCustomFieldFunctions.js'
|
||||
import { getFilename } from './getFilename.js'
|
||||
@@ -23,8 +24,10 @@ export type Export = {
|
||||
format: 'csv' | 'json'
|
||||
globals?: string[]
|
||||
id: number | string
|
||||
limit?: number
|
||||
locale?: string
|
||||
name: string
|
||||
page?: number
|
||||
slug: string
|
||||
sort: Sort
|
||||
user: string
|
||||
@@ -57,6 +60,8 @@ export const createExport = async (args: CreateExportArgs) => {
|
||||
locale: localeInput,
|
||||
sort,
|
||||
user,
|
||||
page,
|
||||
limit: incomingLimit,
|
||||
where,
|
||||
},
|
||||
req: { locale: localeArg, payload },
|
||||
@@ -87,14 +92,30 @@ export const createExport = async (args: CreateExportArgs) => {
|
||||
req.payload.logger.debug({ message: 'Export configuration:', name, isCSV, locale })
|
||||
}
|
||||
|
||||
const batchSize = 100 // fixed per request
|
||||
|
||||
const hardLimit =
|
||||
typeof incomingLimit === 'number' && incomingLimit > 0 ? incomingLimit : undefined
|
||||
|
||||
const { totalDocs } = await payload.count({
|
||||
collection: collectionSlug,
|
||||
user,
|
||||
locale,
|
||||
overrideAccess: false,
|
||||
})
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(totalDocs / batchSize))
|
||||
const requestedPage = page || 1
|
||||
const adjustedPage = requestedPage > totalPages ? 1 : requestedPage
|
||||
|
||||
const findArgs = {
|
||||
collection: collectionSlug,
|
||||
depth: 1,
|
||||
draft: drafts === 'yes',
|
||||
limit: 100,
|
||||
limit: batchSize,
|
||||
locale,
|
||||
overrideAccess: false,
|
||||
page: 0,
|
||||
page: 0, // The page will be incremented manually in the loop
|
||||
select,
|
||||
sort,
|
||||
user,
|
||||
@@ -156,15 +177,37 @@ export const createExport = async (args: CreateExportArgs) => {
|
||||
req.payload.logger.debug('Pre-scanning all columns before streaming')
|
||||
}
|
||||
|
||||
const limitErrorMsg = validateLimitValue(
|
||||
incomingLimit,
|
||||
req.t,
|
||||
batchSize, // step i.e. 100
|
||||
)
|
||||
if (limitErrorMsg) {
|
||||
throw new APIError(limitErrorMsg)
|
||||
}
|
||||
|
||||
const allColumns: string[] = []
|
||||
|
||||
if (isCSV) {
|
||||
const allColumnsSet = new Set<string>()
|
||||
let scanPage = 1
|
||||
|
||||
// Use the incoming page value here, defaulting to 1 if undefined
|
||||
let scanPage = adjustedPage
|
||||
let hasMore = true
|
||||
let fetched = 0
|
||||
const maxDocs = typeof hardLimit === 'number' ? hardLimit : Number.POSITIVE_INFINITY
|
||||
|
||||
while (hasMore) {
|
||||
const result = await payload.find({ ...findArgs, page: scanPage })
|
||||
const remaining = Math.max(0, maxDocs - fetched)
|
||||
if (remaining === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
const result = await payload.find({
|
||||
...findArgs,
|
||||
page: scanPage,
|
||||
limit: Math.min(batchSize, remaining),
|
||||
})
|
||||
|
||||
result.docs.forEach((doc) => {
|
||||
const flat = filterDisabledCSV(flattenObject({ doc, fields, toCSVFunctions }))
|
||||
@@ -176,8 +219,9 @@ export const createExport = async (args: CreateExportArgs) => {
|
||||
})
|
||||
})
|
||||
|
||||
hasMore = result.hasNextPage
|
||||
scanPage += 1
|
||||
fetched += result.docs.length
|
||||
scanPage += 1 // Increment page for next batch
|
||||
hasMore = result.hasNextPage && fetched < maxDocs
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
@@ -187,11 +231,27 @@ export const createExport = async (args: CreateExportArgs) => {
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
let isFirstBatch = true
|
||||
let streamPage = 1
|
||||
let streamPage = adjustedPage
|
||||
let fetched = 0
|
||||
const maxDocs = typeof hardLimit === 'number' ? hardLimit : Number.POSITIVE_INFINITY
|
||||
|
||||
const stream = new Readable({
|
||||
async read() {
|
||||
const result = await payload.find({ ...findArgs, page: streamPage })
|
||||
const remaining = Math.max(0, maxDocs - fetched)
|
||||
|
||||
if (remaining === 0) {
|
||||
if (!isCSV) {
|
||||
this.push(encoder.encode(']'))
|
||||
}
|
||||
this.push(null)
|
||||
return
|
||||
}
|
||||
|
||||
const result = await payload.find({
|
||||
...findArgs,
|
||||
page: streamPage,
|
||||
limit: Math.min(batchSize, remaining),
|
||||
})
|
||||
|
||||
if (debug) {
|
||||
req.payload.logger.debug(`Streaming batch ${streamPage} with ${result.docs.length} docs`)
|
||||
@@ -240,10 +300,11 @@ export const createExport = async (args: CreateExportArgs) => {
|
||||
}
|
||||
}
|
||||
|
||||
fetched += result.docs.length
|
||||
isFirstBatch = false
|
||||
streamPage += 1
|
||||
streamPage += 1 // Increment stream page for the next batch
|
||||
|
||||
if (!result.hasNextPage) {
|
||||
if (!result.hasNextPage || fetched >= maxDocs) {
|
||||
if (debug) {
|
||||
req.payload.logger.debug('Stream complete - no more pages')
|
||||
}
|
||||
@@ -272,18 +333,29 @@ export const createExport = async (args: CreateExportArgs) => {
|
||||
const rows: Record<string, unknown>[] = []
|
||||
const columnsSet = new Set<string>()
|
||||
const columns: string[] = []
|
||||
let page = 1
|
||||
|
||||
// Start from the incoming page value, defaulting to 1 if undefined
|
||||
let currentPage = adjustedPage
|
||||
let fetched = 0
|
||||
let hasNextPage = true
|
||||
const maxDocs = typeof hardLimit === 'number' ? hardLimit : Number.POSITIVE_INFINITY
|
||||
|
||||
while (hasNextPage) {
|
||||
const remaining = Math.max(0, maxDocs - fetched)
|
||||
|
||||
if (remaining === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
const result = await payload.find({
|
||||
...findArgs,
|
||||
page,
|
||||
page: currentPage,
|
||||
limit: Math.min(batchSize, remaining),
|
||||
})
|
||||
|
||||
if (debug) {
|
||||
req.payload.logger.debug(
|
||||
`Processing batch ${findArgs.page} with ${result.docs.length} documents`,
|
||||
`Processing batch ${currentPage} with ${result.docs.length} documents`,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -308,10 +380,12 @@ export const createExport = async (args: CreateExportArgs) => {
|
||||
outputData.push(batchRows.map((doc) => JSON.stringify(doc)).join(',\n'))
|
||||
}
|
||||
|
||||
hasNextPage = result.hasNextPage
|
||||
page += 1
|
||||
fetched += result.docs.length
|
||||
hasNextPage = result.hasNextPage && fetched < maxDocs
|
||||
currentPage += 1 // Increment page for next batch
|
||||
}
|
||||
|
||||
// Prepare final output
|
||||
if (isCSV) {
|
||||
const paddedRows = rows.map((row) => {
|
||||
const fullRow: Record<string, unknown> = {}
|
||||
|
||||
@@ -5,22 +5,33 @@ import { APIError } from 'payload'
|
||||
import { createExport } from './createExport.js'
|
||||
|
||||
export const download = async (req: PayloadRequest, debug = false) => {
|
||||
let body
|
||||
if (typeof req?.json === 'function') {
|
||||
body = await req.json()
|
||||
try {
|
||||
let body
|
||||
if (typeof req?.json === 'function') {
|
||||
body = await req.json()
|
||||
}
|
||||
|
||||
if (!body || !body.data) {
|
||||
throw new APIError('Request data is required.')
|
||||
}
|
||||
|
||||
const { collectionSlug } = body.data || {}
|
||||
|
||||
req.payload.logger.info(`Download request received ${collectionSlug}`)
|
||||
body.data.user = req.user
|
||||
|
||||
const res = await createExport({
|
||||
download: true,
|
||||
input: { ...body.data, debug },
|
||||
req,
|
||||
})
|
||||
|
||||
return res as Response
|
||||
} catch (err) {
|
||||
// Return JSON for front-end toast
|
||||
return new Response(
|
||||
JSON.stringify({ errors: [{ message: (err as Error).message || 'Something went wrong' }] }),
|
||||
{ headers: { 'Content-Type': 'application/json' }, status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
if (!body || !body.data) {
|
||||
throw new APIError('Request data is required.')
|
||||
}
|
||||
|
||||
req.payload.logger.info(`Download request received ${body.data.collectionSlug}`)
|
||||
|
||||
body.data.user = req.user
|
||||
|
||||
return createExport({
|
||||
download: true,
|
||||
input: { ...body.data, debug },
|
||||
req,
|
||||
}) as Promise<Response>
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user