diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
index eb2c6103f5..197ee65ac7 100644
--- a/.github/actions/setup/action.yml
+++ b/.github/actions/setup/action.yml
@@ -6,7 +6,7 @@ inputs:
node-version:
description: Node.js version
required: true
- default: 22.6.0
+ default: 23.11.0
pnpm-version:
description: Pnpm version
required: true
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 223fa6808c..72294e764e 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -16,7 +16,7 @@ concurrency:
cancel-in-progress: true
env:
- NODE_VERSION: 22.6.0
+ NODE_VERSION: 23.11.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
diff --git a/.github/workflows/post-release-templates.yml b/.github/workflows/post-release-templates.yml
index e322f0ed9b..f371e8dab7 100644
--- a/.github/workflows/post-release-templates.yml
+++ b/.github/workflows/post-release-templates.yml
@@ -7,7 +7,7 @@ on:
workflow_dispatch:
env:
- NODE_VERSION: 22.6.0
+ NODE_VERSION: 23.11.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml
index 80477e7a42..f455ad2da2 100644
--- a/.github/workflows/post-release.yml
+++ b/.github/workflows/post-release.yml
@@ -12,7 +12,7 @@ on:
default: ''
env:
- NODE_VERSION: 22.6.0
+ NODE_VERSION: 23.11.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
diff --git a/.github/workflows/publish-prerelease.yml b/.github/workflows/publish-prerelease.yml
index b8575800fb..cdc43c7142 100644
--- a/.github/workflows/publish-prerelease.yml
+++ b/.github/workflows/publish-prerelease.yml
@@ -7,7 +7,7 @@ on:
workflow_dispatch:
env:
- NODE_VERSION: 22.6.0
+ NODE_VERSION: 23.11.0
PNPM_VERSION: 9.7.1
DO_NOT_TRACK: 1 # Disable Turbopack telemetry
NEXT_TELEMETRY_DISABLED: 1 # Disable Next telemetry
diff --git a/.node-version b/.node-version
index 535e1cca76..5afafd98f8 100644
--- a/.node-version
+++ b/.node-version
@@ -1 +1 @@
-v22.6.0
+v23.11.0
diff --git a/.nvmrc b/.nvmrc
index 535e1cca76..5afafd98f8 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-v22.6.0
+v23.11.0
diff --git a/.tool-versions b/.tool-versions
index 4e644f8f56..51d91bdccc 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,2 +1,2 @@
pnpm 9.7.1
-nodejs 22.6.0
+nodejs 23.11.0
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 0180e9a7b9..26980151e0 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -63,6 +63,13 @@
"request": "launch",
"type": "node-terminal"
},
+ {
+ "command": "pnpm tsx --no-deprecation test/dev.ts query-presets",
+ "cwd": "${workspaceFolder}",
+ "name": "Run Dev Query Presets",
+ "request": "launch",
+ "type": "node-terminal"
+ },
{
"command": "pnpm tsx --no-deprecation test/dev.ts login-with-username",
"cwd": "${workspaceFolder}",
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b1311e3825..7d4c0086bf 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -24,5 +24,8 @@
"runtimeArgs": ["--no-deprecation"]
},
// Essentially disables bun test buttons
- "bun.test.filePattern": "bun.test.ts"
+ "bun.test.filePattern": "bun.test.ts",
+ "playwright.env": {
+ "NODE_OPTIONS": "--no-deprecation --no-experimental-strip-types"
+ }
}
diff --git a/docs/fields/group.mdx b/docs/fields/group.mdx
index 9062dc6f5d..7865e0fc75 100644
--- a/docs/fields/group.mdx
+++ b/docs/fields/group.mdx
@@ -35,9 +35,9 @@ export const MyGroupField: Field = {
| Option | Description |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
+| **`name`** | To be used as the property name when stored and retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`fields`** \* | Array of field types to nest within this Group. |
-| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. |
+| **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. Required when name is undefined, defaults to name converted to words. |
| **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More](/docs/fields/overview#validation) |
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/overview), include its data in the user JWT. |
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
@@ -86,7 +86,7 @@ export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
- name: 'pageMeta', // required
+ name: 'pageMeta',
type: 'group', // required
interfaceName: 'Meta', // optional
fields: [
@@ -110,3 +110,38 @@ export const ExampleCollection: CollectionConfig = {
],
}
```
+
+## Presentational group fields
+
+You can also use the Group field to create a presentational group of fields. This is useful when you want to group fields together visually without affecting the data structure.
+The label will be required when a `name` is not provided.
+
+```ts
+import type { CollectionConfig } from 'payload'
+
+export const ExampleCollection: CollectionConfig = {
+ slug: 'example-collection',
+ fields: [
+ {
+ label: 'Page meta',
+ type: 'group', // required
+ fields: [
+ {
+ name: 'title',
+ type: 'text',
+ required: true,
+ minLength: 20,
+ maxLength: 100,
+ },
+ {
+ name: 'description',
+ type: 'textarea',
+ required: true,
+ minLength: 40,
+ maxLength: 160,
+ },
+ ],
+ },
+ ],
+}
+```
diff --git a/docs/integrations/vercel-content-link.mdx b/docs/integrations/vercel-content-link.mdx
index fb33a876ca..24d077a616 100644
--- a/docs/integrations/vercel-content-link.mdx
+++ b/docs/integrations/vercel-content-link.mdx
@@ -63,19 +63,50 @@ const config = buildConfig({
export default config
```
-Now in your Next.js app, include the `?encodeSourceMaps=true` parameter in any of your API requests. For performance reasons, this should only be done when in draft mode or on preview deployments.
+## Enabling Content Source Maps
+
+Now in your Next.js app, you need to add the `encodeSourceMaps` query parameter to your API requests. This will tell Payload to include the Content Source Maps in the API response.
+
+
+ **Note:** For performance reasons, this should only be done when in draft mode
+ or on preview deployments.
+
+
+#### REST API
+
+If you're using the REST API, include the `?encodeSourceMaps=true` search parameter.
```ts
if (isDraftMode || process.env.VERCEL_ENV === 'preview') {
const res = await fetch(
- `${process.env.NEXT_PUBLIC_PAYLOAD_CMS_URL}/api/pages?where[slug][equals]=${slug}&encodeSourceMaps=true`,
+ `${process.env.NEXT_PUBLIC_PAYLOAD_CMS_URL}/api/pages?encodeSourceMaps=true&where[slug][equals]=${slug}`,
)
}
```
+#### Local API
+
+If you're using the Local API, include the `encodeSourceMaps` via the `context` property.
+
+```ts
+if (isDraftMode || process.env.VERCEL_ENV === 'preview') {
+ const res = await payload.find({
+ collection: 'pages',
+ where: {
+ slug: {
+ equals: slug,
+ },
+ },
+ context: {
+ encodeSourceMaps: true,
+ },
+ })
+}
+```
+
And that's it! You are now ready to enter Edit Mode and begin visually editing your content.
-#### Edit Mode
+## Edit Mode
To see Content Link on your site, you first need to visit any preview deployment on Vercel and login using the Vercel Toolbar. When Content Source Maps are detected on the page, a pencil icon will appear in the toolbar. Clicking this icon will enable Edit Mode, highlighting all editable fields on the page in blue.
@@ -94,7 +125,9 @@ const { cleaned, encoded } = vercelStegaSplit(text)
### Blocks and array fields
-All `blocks` and `array` fields by definition do not have plain text strings to encode. For this reason, they are given an additional `_encodedSourceMap` property, which you can use to enable Content Link on entire _sections_ of your site. You can then specify the editing container by adding the `data-vercel-edit-target` HTML attribute to any top-level element of your block.
+All `blocks` and `array` fields by definition do not have plain text strings to encode. For this reason, they are automatically given an additional `_encodedSourceMap` property, which you can use to enable Content Link on entire _sections_ of your site.
+
+You can then specify the editing container by adding the `data-vercel-edit-target` HTML attribute to any top-level element of your block.
```ts
diff --git a/docs/plugins/stripe.mdx b/docs/plugins/stripe.mdx
index bb7a4141a8..214267f0a2 100644
--- a/docs/plugins/stripe.mdx
+++ b/docs/plugins/stripe.mdx
@@ -309,7 +309,3 @@ import {
...
} from '@payloadcms/plugin-stripe/types';
```
-
-## Examples
-
-The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) contains an official [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommerce) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end. You can also check out [How to Build An E-Commerce Site With Next.js](https://payloadcms.com/blog/how-to-build-an-e-commerce-site-with-nextjs) post for a bit more context around this template.
diff --git a/docs/queries/pagination.mdx b/docs/queries/pagination.mdx
index e3549d5f3a..a9f39137db 100644
--- a/docs/queries/pagination.mdx
+++ b/docs/queries/pagination.mdx
@@ -55,10 +55,11 @@ All collection `find` queries are paginated automatically. Responses are returne
All Payload APIs support the pagination controls below. With them, you can create paginated lists of documents within your application:
-| Control | Description |
-| ------- | --------------------------------------- |
-| `limit` | Limits the number of documents returned |
-| `page` | Get a specific page number |
+| Control | Default | Description |
+| ------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `limit` | `10` | Limits the number of documents returned per page - set to `0` to show all documents, we automatically disabled pagination for you when `limit` is `0` for optimisation |
+| `pagination` | `true` | Set to `false` to disable pagination and return all documents |
+| `page` | `1` | Get a specific page number |
### Disabling pagination within Local API
diff --git a/docs/upload/storage-adapters.mdx b/docs/upload/storage-adapters.mdx
index 35c29d9d64..3d4f3639a2 100644
--- a/docs/upload/storage-adapters.mdx
+++ b/docs/upload/storage-adapters.mdx
@@ -84,6 +84,7 @@ pnpm add @payloadcms/storage-s3
- The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information.
- When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection.
- When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website.
+- Configure `signedDownloads` (either globally of per-collection in `collections`) to use [presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html) for files downloading. This can improve performance for large files (like videos) while still respecting your access control.
```ts
import { s3Storage } from '@payloadcms/storage-s3'
diff --git a/eslint.config.js b/eslint.config.js
index b63fa17cc4..f808f18b4d 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -74,6 +74,7 @@ export const rootEslintConfig = [
'no-console': 'off',
'perfectionist/sort-object-types': 'off',
'perfectionist/sort-objects': 'off',
+ 'payload/no-relative-monorepo-imports': 'off',
},
},
]
diff --git a/examples/localization/src/search/beforeSync.ts b/examples/localization/src/search/beforeSync.ts
index d0c03c83c1..2d9c7be7ef 100644
--- a/examples/localization/src/search/beforeSync.ts
+++ b/examples/localization/src/search/beforeSync.ts
@@ -1,11 +1,11 @@
import { BeforeSync, DocToSync } from '@payloadcms/plugin-search/types'
-export const beforeSyncWithSearch: BeforeSync = async ({ originalDoc, searchDoc, payload }) => {
+export const beforeSyncWithSearch: BeforeSync = async ({ req, originalDoc, searchDoc }) => {
const {
doc: { relationTo: collection },
} = searchDoc
- const { slug, id, categories, title, meta, excerpt } = originalDoc
+ const { slug, id, categories, title, meta } = originalDoc
const modifiedDoc: DocToSync = {
...searchDoc,
@@ -20,24 +20,40 @@ export const beforeSyncWithSearch: BeforeSync = async ({ originalDoc, searchDoc,
}
if (categories && Array.isArray(categories) && categories.length > 0) {
- // get full categories and keep a flattened copy of their most important properties
- try {
- const mappedCategories = categories.map((category) => {
- const { id, title } = category
+ const populatedCategories: { id: string | number; title: string }[] = []
+ for (const category of categories) {
+ if (!category) {
+ continue
+ }
- return {
- relationTo: 'categories',
- id,
- title,
- }
+ if (typeof category === 'object') {
+ populatedCategories.push(category)
+ continue
+ }
+
+ const doc = await req.payload.findByID({
+ collection: 'categories',
+ id: category,
+ disableErrors: true,
+ depth: 0,
+ select: { title: true },
+ req,
})
- modifiedDoc.categories = mappedCategories
- } catch (err) {
- console.error(
- `Failed. Category not found when syncing collection '${collection}' with id: '${id}' to search.`,
- )
+ if (doc !== null) {
+ populatedCategories.push(doc)
+ } else {
+ console.error(
+ `Failed. Category not found when syncing collection '${collection}' with id: '${id}' to search.`,
+ )
+ }
}
+
+ modifiedDoc.categories = populatedCategories.map((each) => ({
+ relationTo: 'categories',
+ categoryID: String(each.id),
+ title: each.title,
+ }))
}
return modifiedDoc
diff --git a/examples/localization/src/search/fieldOverrides.ts b/examples/localization/src/search/fieldOverrides.ts
index d5c1e98ee0..fb9b7a2ad4 100644
--- a/examples/localization/src/search/fieldOverrides.ts
+++ b/examples/localization/src/search/fieldOverrides.ts
@@ -52,7 +52,7 @@ export const searchFields: Field[] = [
type: 'text',
},
{
- name: 'id',
+ name: 'categoryID',
type: 'text',
},
{
diff --git a/package.json b/package.json
index a25e17adc3..34cfdc72de 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
- "version": "3.36.1",
+ "version": "3.38.0",
"private": true,
"type": "module",
"scripts": {
diff --git a/packages/admin-bar/package.json b/packages/admin-bar/package.json
index c4a3947edf..87864c5944 100644
--- a/packages/admin-bar/package.json
+++ b/packages/admin-bar/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/admin-bar",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "An admin bar for React apps using Payload",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/create-payload-app/package.json b/packages/create-payload-app/package.json
index cec49d40be..95c959166d 100644
--- a/packages/create-payload-app/package.json
+++ b/packages/create-payload-app/package.json
@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
- "version": "3.36.1",
+ "version": "3.38.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
diff --git a/packages/db-mongodb/package.json b/packages/db-mongodb/package.json
index bba9c0dd54..f3e2358789 100644
--- a/packages/db-mongodb/package.json
+++ b/packages/db-mongodb/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/db-mongodb/src/models/buildSchema.ts b/packages/db-mongodb/src/models/buildSchema.ts
index 4cd833ccec..56e2cf1130 100644
--- a/packages/db-mongodb/src/models/buildSchema.ts
+++ b/packages/db-mongodb/src/models/buildSchema.ts
@@ -372,36 +372,61 @@ const group: FieldSchemaGenerator = (
buildSchemaOptions,
parentIsLocalized,
): void => {
- const formattedBaseSchema = formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized })
+ if (fieldAffectsData(field)) {
+ const formattedBaseSchema = formatBaseSchema({ buildSchemaOptions, field, parentIsLocalized })
- // carry indexSortableFields through to versions if drafts enabled
- const indexSortableFields =
- buildSchemaOptions.indexSortableFields &&
- field.name === 'version' &&
- buildSchemaOptions.draftsEnabled
+ // carry indexSortableFields through to versions if drafts enabled
+ const indexSortableFields =
+ buildSchemaOptions.indexSortableFields &&
+ field.name === 'version' &&
+ buildSchemaOptions.draftsEnabled
- const baseSchema: SchemaTypeOptions = {
- ...formattedBaseSchema,
- type: buildSchema({
- buildSchemaOptions: {
- disableUnique: buildSchemaOptions.disableUnique,
- draftsEnabled: buildSchemaOptions.draftsEnabled,
- indexSortableFields,
- options: {
- _id: false,
- id: false,
- minimize: false,
+ const baseSchema: SchemaTypeOptions = {
+ ...formattedBaseSchema,
+ type: buildSchema({
+ buildSchemaOptions: {
+ disableUnique: buildSchemaOptions.disableUnique,
+ draftsEnabled: buildSchemaOptions.draftsEnabled,
+ indexSortableFields,
+ options: {
+ _id: false,
+ id: false,
+ minimize: false,
+ },
},
- },
- configFields: field.fields,
- parentIsLocalized: parentIsLocalized || field.localized,
- payload,
- }),
- }
+ configFields: field.fields,
+ parentIsLocalized: parentIsLocalized || field.localized,
+ payload,
+ }),
+ }
- schema.add({
- [field.name]: localizeSchema(field, baseSchema, payload.config.localization, parentIsLocalized),
- })
+ schema.add({
+ [field.name]: localizeSchema(
+ field,
+ baseSchema,
+ payload.config.localization,
+ parentIsLocalized,
+ ),
+ })
+ } else {
+ field.fields.forEach((subField) => {
+ if (fieldIsVirtual(subField)) {
+ return
+ }
+
+ const addFieldSchema = getSchemaGenerator(subField.type)
+
+ if (addFieldSchema) {
+ addFieldSchema(
+ subField,
+ schema,
+ payload,
+ buildSchemaOptions,
+ (parentIsLocalized || field.localized) ?? false,
+ )
+ }
+ })
+ }
}
const json: FieldSchemaGenerator = (
diff --git a/packages/db-mongodb/src/queries/buildSortParam.ts b/packages/db-mongodb/src/queries/buildSortParam.ts
index e629b0b42e..7b78068777 100644
--- a/packages/db-mongodb/src/queries/buildSortParam.ts
+++ b/packages/db-mongodb/src/queries/buildSortParam.ts
@@ -57,12 +57,8 @@ const relationshipSort = ({
return false
}
- for (const [i, segment] of segments.entries()) {
- if (versions && i === 0 && segment === 'version') {
- segments.shift()
- continue
- }
-
+ for (let i = 0; i < segments.length; i++) {
+ const segment = segments[i]
const field = currentFields.find((each) => each.name === segment)
if (!field) {
@@ -71,6 +67,10 @@ const relationshipSort = ({
if ('fields' in field) {
currentFields = field.flattenedFields
+ if (field.name === 'version' && versions && i === 0) {
+ segments.shift()
+ i--
+ }
} else if (
(field.type === 'relationship' || field.type === 'upload') &&
i !== segments.length - 1
@@ -106,7 +106,7 @@ const relationshipSort = ({
as: `__${path}`,
foreignField: '_id',
from: foreignCollection.Model.collection.name,
- localField: relationshipPath,
+ localField: versions ? `version.${relationshipPath}` : relationshipPath,
pipeline: [
{
$project: {
diff --git a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts
index c17c239b73..3de2aa6cba 100644
--- a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts
+++ b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts
@@ -105,6 +105,7 @@ export const sanitizeQueryValue = ({
| undefined => {
let formattedValue = val
let formattedOperator = operator
+
if (['array', 'blocks', 'group', 'tab'].includes(field.type) && path.includes('.')) {
const segments = path.split('.')
segments.shift()
diff --git a/packages/db-mongodb/src/queryDrafts.ts b/packages/db-mongodb/src/queryDrafts.ts
index 685bfa1ddb..c43e0c52f4 100644
--- a/packages/db-mongodb/src/queryDrafts.ts
+++ b/packages/db-mongodb/src/queryDrafts.ts
@@ -151,6 +151,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
query: versionQuery,
session: paginationOptions.options?.session ?? undefined,
sort: paginationOptions.sort as object,
+ sortAggregation,
useEstimatedCount: paginationOptions.useEstimatedCount,
})
} else {
diff --git a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts
index 98a659643c..bd80cc99d7 100644
--- a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts
+++ b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts
@@ -128,7 +128,6 @@ const traverseFields = ({
break
}
-
case 'blocks': {
const blocksSelect = select[field.name] as SelectType
diff --git a/packages/db-mongodb/src/utilities/transform.ts b/packages/db-mongodb/src/utilities/transform.ts
index 7f1dae84a7..e5633f1acf 100644
--- a/packages/db-mongodb/src/utilities/transform.ts
+++ b/packages/db-mongodb/src/utilities/transform.ts
@@ -425,6 +425,7 @@ export const transform = ({
for (const locale of config.localization.localeCodes) {
sanitizeDate({
field,
+ locale,
ref: fieldRef,
value: fieldRef[locale],
})
diff --git a/packages/db-postgres/package.json b/packages/db-postgres/package.json
index 51a933ad4c..04e8f89f36 100644
--- a/packages/db-postgres/package.json
+++ b/packages/db-postgres/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/db-sqlite/package.json b/packages/db-sqlite/package.json
index 87db6a8f0e..d3555bfda6 100644
--- a/packages/db-sqlite/package.json
+++ b/packages/db-sqlite/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/db-vercel-postgres/package.json b/packages/db-vercel-postgres/package.json
index 1268e2c95c..3d1e51c536 100644
--- a/packages/db-vercel-postgres/package.json
+++ b/packages/db-vercel-postgres/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/drizzle/package.json b/packages/drizzle/package.json
index 254394b989..2024ca9217 100644
--- a/packages/drizzle/package.json
+++ b/packages/drizzle/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/drizzle",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "A library of shared functions used by different payload database adapters",
"homepage": "https://payloadcms.com",
"repository": {
@@ -53,6 +53,7 @@
},
"dependencies": {
"console-table-printer": "2.12.1",
+ "dequal": "2.0.3",
"drizzle-orm": "0.36.1",
"prompts": "2.4.2",
"to-snake-case": "1.0.0",
diff --git a/packages/drizzle/src/find/findMany.ts b/packages/drizzle/src/find/findMany.ts
index 6b429f38ca..d1374f42a6 100644
--- a/packages/drizzle/src/find/findMany.ts
+++ b/packages/drizzle/src/find/findMany.ts
@@ -7,6 +7,7 @@ import type { DrizzleAdapter } from '../types.js'
import buildQuery from '../queries/buildQuery.js'
import { selectDistinct } from '../queries/selectDistinct.js'
import { transform } from '../transform/read/index.js'
+import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
import { getTransaction } from '../utilities/getTransaction.js'
import { buildFindManyArgs } from './buildFindManyArgs.js'
@@ -75,6 +76,26 @@ export const findMany = async function find({
tableName,
versions,
})
+
+ if (orderBy) {
+ for (const key in selectFields) {
+ const column = selectFields[key]
+ if (column.primary) {
+ continue
+ }
+
+ if (
+ !orderBy.some(
+ (col) =>
+ col.column.name === column.name &&
+ getNameFromDrizzleTable(col.column.table) === getNameFromDrizzleTable(column.table),
+ )
+ ) {
+ delete selectFields[key]
+ }
+ }
+ }
+
const selectDistinctResult = await selectDistinct({
adapter,
db,
diff --git a/packages/drizzle/src/find/traverseFields.ts b/packages/drizzle/src/find/traverseFields.ts
index 5d1c1bd170..d6ad716239 100644
--- a/packages/drizzle/src/find/traverseFields.ts
+++ b/packages/drizzle/src/find/traverseFields.ts
@@ -22,6 +22,7 @@ import type { Result } from './buildFindManyArgs.js'
import buildQuery from '../queries/buildQuery.js'
import { getTableAlias } from '../queries/getTableAlias.js'
import { operatorMap } from '../queries/operatorMap.js'
+import { getArrayRelationName } from '../utilities/getArrayRelationName.js'
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
import { jsonAggBuildObject } from '../utilities/json.js'
import { rawConstraint } from '../utilities/rawConstraint.js'
@@ -196,7 +197,12 @@ export const traverseFields = ({
}
}
- const relationName = field.dbName ? `_${arrayTableName}` : `${path}${field.name}`
+ const relationName = getArrayRelationName({
+ field,
+ path: `${path}${field.name}`,
+ tableName: arrayTableName,
+ })
+
currentArgs.with[relationName] = withArray
traverseFields({
diff --git a/packages/drizzle/src/migrate.ts b/packages/drizzle/src/migrate.ts
index 0256031ac6..fa422f42c6 100644
--- a/packages/drizzle/src/migrate.ts
+++ b/packages/drizzle/src/migrate.ts
@@ -42,33 +42,36 @@ export const migrate: DrizzleAdapter['migrate'] = async function migrate(
limit: 0,
sort: '-name',
}))
+
+ if (migrationsInDB.find((m) => m.batch === -1)) {
+ const { confirm: runMigrations } = await prompts(
+ {
+ name: 'confirm',
+ type: 'confirm',
+ initial: false,
+ message:
+ "It looks like you've run Payload in dev mode, meaning you've dynamically pushed changes to your database.\n\n" +
+ "If you'd like to run migrations, data loss will occur. Would you like to proceed?",
+ },
+ {
+ onCancel: () => {
+ process.exit(0)
+ },
+ },
+ )
+
+ if (!runMigrations) {
+ process.exit(0)
+ }
+ // ignore the dev migration so that the latest batch number increments correctly
+ migrationsInDB = migrationsInDB.filter((m) => m.batch !== -1)
+ }
+
if (Number(migrationsInDB?.[0]?.batch) > 0) {
latestBatch = Number(migrationsInDB[0]?.batch)
}
}
- if (migrationsInDB.find((m) => m.batch === -1)) {
- const { confirm: runMigrations } = await prompts(
- {
- name: 'confirm',
- type: 'confirm',
- initial: false,
- message:
- "It looks like you've run Payload in dev mode, meaning you've dynamically pushed changes to your database.\n\n" +
- "If you'd like to run migrations, data loss will occur. Would you like to proceed?",
- },
- {
- onCancel: () => {
- process.exit(0)
- },
- },
- )
-
- if (!runMigrations) {
- process.exit(0)
- }
- }
-
const newBatch = latestBatch + 1
// Execute 'up' function for each migration sequentially
diff --git a/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/groupUpSQLStatements.ts b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/groupUpSQLStatements.ts
index 0a9bfe2fea..e4c6f8accb 100644
--- a/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/groupUpSQLStatements.ts
+++ b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/groupUpSQLStatements.ts
@@ -1,49 +1,126 @@
export type Groups =
| 'addColumn'
| 'addConstraint'
+ | 'alterType'
+ | 'createIndex'
+ | 'createTable'
+ | 'createType'
+ | 'disableRowSecurity'
| 'dropColumn'
| 'dropConstraint'
+ | 'dropIndex'
| 'dropTable'
+ | 'dropType'
| 'notNull'
+ | 'renameColumn'
+ | 'setDefault'
/**
- * Convert an "ADD COLUMN" statement to an "ALTER COLUMN" statement
- * example: ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL;
- * to: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
- * @param sql
+ * Convert an "ADD COLUMN" statement to an "ALTER COLUMN" statement.
+ * Works with or without a schema name.
+ *
+ * Examples:
+ * 'ALTER TABLE "pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL;'
+ * => 'ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;'
+ *
+ * 'ALTER TABLE "public"."pages_blocks_my_block" ADD COLUMN "person_id" integer NOT NULL;'
+ * => 'ALTER TABLE "public"."pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;'
*/
function convertAddColumnToAlterColumn(sql) {
// Regular expression to match the ADD COLUMN statement with its constraints
- const regex = /ALTER TABLE ("[^"]+")\.(".*?") ADD COLUMN ("[^"]+") [\w\s]+ NOT NULL;/
+ const regex = /ALTER TABLE ((?:"[^"]+"\.)?"[^"]+") ADD COLUMN ("[^"]+") [^;]*?NOT NULL;/i
// Replace the matched part with "ALTER COLUMN ... SET NOT NULL;"
- return sql.replace(regex, 'ALTER TABLE $1.$2 ALTER COLUMN $3 SET NOT NULL;')
+ return sql.replace(regex, 'ALTER TABLE $1 ALTER COLUMN $2 SET NOT NULL;')
}
export const groupUpSQLStatements = (list: string[]): Record => {
const groups = {
+ /**
+ * example: ALTER TABLE "posts" ADD COLUMN "category_id" integer
+ */
addColumn: 'ADD COLUMN',
- // example: ALTER TABLE "posts" ADD COLUMN "category_id" integer
+ /**
+ * example:
+ * DO $$ BEGIN
+ * ALTER TABLE "pages_blocks_my_block" ADD CONSTRAINT "pages_blocks_my_block_person_id_users_id_fk" FOREIGN KEY ("person_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
+ * EXCEPTION
+ * WHEN duplicate_object THEN null;
+ * END $$;
+ */
addConstraint: 'ADD CONSTRAINT',
- //example:
- // DO $$ BEGIN
- // ALTER TABLE "pages_blocks_my_block" ADD CONSTRAINT "pages_blocks_my_block_person_id_users_id_fk" FOREIGN KEY ("person_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;
- // EXCEPTION
- // WHEN duplicate_object THEN null;
- // END $$;
+ /**
+ * example: CREATE TABLE IF NOT EXISTS "payload_locked_documents" (
+ * "id" serial PRIMARY KEY NOT NULL,
+ * "global_slug" varchar,
+ * "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
+ * "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
+ * );
+ */
+ createTable: 'CREATE TABLE',
+
+ /**
+ * example: ALTER TABLE "_posts_v_rels" DROP COLUMN IF EXISTS "posts_id";
+ */
dropColumn: 'DROP COLUMN',
- // example: ALTER TABLE "_posts_v_rels" DROP COLUMN IF EXISTS "posts_id";
+ /**
+ * example: ALTER TABLE "_posts_v_rels" DROP CONSTRAINT "_posts_v_rels_posts_fk";
+ */
dropConstraint: 'DROP CONSTRAINT',
- // example: ALTER TABLE "_posts_v_rels" DROP CONSTRAINT "_posts_v_rels_posts_fk";
+ /**
+ * example: DROP TABLE "pages_rels";
+ */
dropTable: 'DROP TABLE',
- // example: DROP TABLE "pages_rels";
+ /**
+ * example: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
+ */
notNull: 'NOT NULL',
- // example: ALTER TABLE "pages_blocks_my_block" ALTER COLUMN "person_id" SET NOT NULL;
+
+ /**
+ * example: CREATE TYPE "public"."enum__pages_v_published_locale" AS ENUM('en', 'es');
+ */
+ createType: 'CREATE TYPE',
+
+ /**
+ * example: ALTER TYPE "public"."enum_pages_blocks_cta" ADD VALUE 'copy';
+ */
+ alterType: 'ALTER TYPE',
+
+ /**
+ * example: ALTER TABLE "categories_rels" DISABLE ROW LEVEL SECURITY;
+ */
+ disableRowSecurity: 'DISABLE ROW LEVEL SECURITY;',
+
+ /**
+ * example: DROP INDEX IF EXISTS "pages_title_idx";
+ */
+ dropIndex: 'DROP INDEX IF EXISTS',
+
+ /**
+ * example: ALTER TABLE "pages" ALTER COLUMN "_status" SET DEFAULT 'draft';
+ */
+ setDefault: 'SET DEFAULT',
+
+ /**
+ * example: CREATE INDEX IF NOT EXISTS "payload_locked_documents_global_slug_idx" ON "payload_locked_documents" USING btree ("global_slug");
+ */
+ createIndex: 'INDEX IF NOT EXISTS',
+
+ /**
+ * example: DROP TYPE "public"."enum__pages_v_published_locale";
+ */
+ dropType: 'DROP TYPE',
+
+ /**
+ * columns were renamed from camelCase to snake_case
+ * example: ALTER TABLE "forms" RENAME COLUMN "confirmationType" TO "confirmation_type";
+ */
+ renameColumn: 'RENAME COLUMN',
}
const result = Object.keys(groups).reduce((result, group: Groups) => {
@@ -51,7 +128,17 @@ export const groupUpSQLStatements = (list: string[]): Record =
return result
}, {}) as Record
+ // push multi-line changes to a single grouping
+ let isCreateTable = false
+
for (const line of list) {
+ if (isCreateTable) {
+ result.createTable.push(line)
+ if (line.includes(');')) {
+ isCreateTable = false
+ }
+ continue
+ }
Object.entries(groups).some(([key, value]) => {
if (line.endsWith('NOT NULL;')) {
// split up the ADD COLUMN and ALTER COLUMN NOT NULL statements
@@ -64,7 +151,11 @@ export const groupUpSQLStatements = (list: string[]): Record =
return true
}
if (line.includes(value)) {
- result[key].push(line)
+ let statement = line
+ if (key === 'dropConstraint') {
+ statement = line.replace('" DROP CONSTRAINT "', '" DROP CONSTRAINT IF EXISTS "')
+ }
+ result[key].push(statement)
return true
}
})
diff --git a/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/index.ts b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/index.ts
index 06fc16a9ba..9b68c5d4f6 100644
--- a/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/index.ts
+++ b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/index.ts
@@ -20,6 +20,17 @@ type Args = {
req?: Partial
}
+const runStatementGroup = async ({ adapter, db, debug, statements }) => {
+ const addColumnsStatement = statements.join('\n')
+
+ if (debug) {
+ adapter.payload.logger.info(debug)
+ adapter.payload.logger.info(addColumnsStatement)
+ }
+
+ await db.execute(sql.raw(addColumnsStatement))
+}
+
/**
* Moves upload and relationship columns from the join table and into the tables while moving data
* This is done in the following order:
@@ -36,21 +47,11 @@ type Args = {
*/
export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
const adapter = payload.db as unknown as BasePostgresAdapter
- const db = await getTransaction(adapter, req)
const dir = payload.db.migrationDir
// get the drizzle migrateUpSQL from drizzle using the last schema
const { generateDrizzleJson, generateMigration, upSnapshot } = adapter.requireDrizzleKit()
-
- const toSnapshot: Record = {}
-
- for (const key of Object.keys(adapter.schema).filter(
- (key) => !key.startsWith('payload_locked_documents'),
- )) {
- toSnapshot[key] = adapter.schema[key]
- }
-
- const drizzleJsonAfter = generateDrizzleJson(toSnapshot) as DrizzleSnapshotJSON
+ const drizzleJsonAfter = generateDrizzleJson(adapter.schema) as DrizzleSnapshotJSON
// Get the previous migration snapshot
const previousSnapshot = fs
@@ -82,16 +83,62 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
const sqlUpStatements = groupUpSQLStatements(generatedSQL)
- const addColumnsStatement = sqlUpStatements.addColumn.join('\n')
+ const db = await getTransaction(adapter, req)
- if (debug) {
- payload.logger.info('CREATING NEW RELATIONSHIP COLUMNS')
- payload.logger.info(addColumnsStatement)
- }
+ await runStatementGroup({
+ adapter,
+ db,
+ debug: debug ? 'CREATING TYPES' : null,
+ statements: sqlUpStatements.createType,
+ })
- await db.execute(sql.raw(addColumnsStatement))
+ await runStatementGroup({
+ adapter,
+ db,
+ debug: debug ? 'ALTERING TYPES' : null,
+ statements: sqlUpStatements.alterType,
+ })
+
+ await runStatementGroup({
+ adapter,
+ db,
+ debug: debug ? 'CREATING TABLES' : null,
+ statements: sqlUpStatements.createTable,
+ })
+
+ await runStatementGroup({
+ adapter,
+ db,
+ debug: debug ? 'RENAMING COLUMNS' : null,
+ statements: sqlUpStatements.renameColumn,
+ })
+
+ await runStatementGroup({
+ adapter,
+ db,
+ debug: debug ? 'CREATING NEW RELATIONSHIP COLUMNS' : null,
+ statements: sqlUpStatements.addColumn,
+ })
+
+ // SET DEFAULTS
+ await runStatementGroup({
+ adapter,
+ db,
+ debug: debug ? 'SETTING DEFAULTS' : null,
+ statements: sqlUpStatements.setDefault,
+ })
+
+ await runStatementGroup({
+ adapter,
+ db,
+ debug: debug ? 'CREATING INDEXES' : null,
+ statements: sqlUpStatements.createIndex,
+ })
for (const collection of payload.config.collections) {
+ if (collection.slug === 'payload-locked-documents') {
+ continue
+ }
const tableName = adapter.tableNameMap.get(toSnakeCase(collection.slug))
const pathsToQuery: PathsToQuery = new Set()
@@ -237,52 +284,58 @@ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => {
}
// ADD CONSTRAINT
- const addConstraintsStatement = sqlUpStatements.addConstraint.join('\n')
-
- if (debug) {
- payload.logger.info('ADDING CONSTRAINTS')
- payload.logger.info(addConstraintsStatement)
- }
-
- await db.execute(sql.raw(addConstraintsStatement))
+ await runStatementGroup({
+ adapter,
+ db,
+ debug: debug ? 'ADDING CONSTRAINTS' : null,
+ statements: sqlUpStatements.addConstraint,
+ })
// NOT NULL
- const notNullStatements = sqlUpStatements.notNull.join('\n')
-
- if (debug) {
- payload.logger.info('NOT NULL CONSTRAINTS')
- payload.logger.info(notNullStatements)
- }
-
- await db.execute(sql.raw(notNullStatements))
+ await runStatementGroup({
+ adapter,
+ db,
+ debug: debug ? 'NOT NULL CONSTRAINTS' : null,
+ statements: sqlUpStatements.notNull,
+ })
// DROP TABLE
- const dropTablesStatement = sqlUpStatements.dropTable.join('\n')
+ await runStatementGroup({
+ adapter,
+ db,
+ debug: debug ? 'DROPPING TABLES' : null,
+ statements: sqlUpStatements.dropTable,
+ })
- if (debug) {
- payload.logger.info('DROPPING TABLES')
- payload.logger.info(dropTablesStatement)
- }
-
- await db.execute(sql.raw(dropTablesStatement))
+ // DROP INDEX
+ await runStatementGroup({
+ adapter,
+ db,
+ debug: debug ? 'DROPPING INDEXES' : null,
+ statements: sqlUpStatements.dropIndex,
+ })
// DROP CONSTRAINT
- const dropConstraintsStatement = sqlUpStatements.dropConstraint.join('\n')
-
- if (debug) {
- payload.logger.info('DROPPING CONSTRAINTS')
- payload.logger.info(dropConstraintsStatement)
- }
-
- await db.execute(sql.raw(dropConstraintsStatement))
+ await runStatementGroup({
+ adapter,
+ db,
+ debug: debug ? 'DROPPING CONSTRAINTS' : null,
+ statements: sqlUpStatements.dropConstraint,
+ })
// DROP COLUMN
- const dropColumnsStatement = sqlUpStatements.dropColumn.join('\n')
+ await runStatementGroup({
+ adapter,
+ db,
+ debug: debug ? 'DROPPING COLUMNS' : null,
+ statements: sqlUpStatements.dropColumn,
+ })
- if (debug) {
- payload.logger.info('DROPPING COLUMNS')
- payload.logger.info(dropColumnsStatement)
- }
-
- await db.execute(sql.raw(dropColumnsStatement))
+ // DROP TYPES
+ await runStatementGroup({
+ adapter,
+ db,
+ debug: debug ? 'DROPPING TYPES' : null,
+ statements: sqlUpStatements.dropType,
+ })
}
diff --git a/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/migrateRelationships.ts b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/migrateRelationships.ts
index 7295ac95fc..3ba98d164e 100644
--- a/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/migrateRelationships.ts
+++ b/packages/drizzle/src/postgres/predefinedMigrations/v2-v3/migrateRelationships.ts
@@ -56,7 +56,7 @@ export const migrateRelationships = async ({
${where} ORDER BY parent_id LIMIT 500 OFFSET ${offset * 500};
`
- paginationResult = await adapter.drizzle.execute(sql.raw(`${paginationStatement}`))
+ paginationResult = await db.execute(sql.raw(`${paginationStatement}`))
if (paginationResult.rows.length === 0) {
return
@@ -72,7 +72,7 @@ export const migrateRelationships = async ({
payload.logger.info(statement)
}
- const result = await adapter.drizzle.execute(sql.raw(`${statement}`))
+ const result = await db.execute(sql.raw(`${statement}`))
const docsToResave: DocsToResave = {}
diff --git a/packages/drizzle/src/queries/buildOrderBy.ts b/packages/drizzle/src/queries/buildOrderBy.ts
index da779bbb20..758b8854ce 100644
--- a/packages/drizzle/src/queries/buildOrderBy.ts
+++ b/packages/drizzle/src/queries/buildOrderBy.ts
@@ -1,7 +1,7 @@
-import type { Table } from 'drizzle-orm'
+import type { SQL, Table } from 'drizzle-orm'
import type { FlattenedField, Sort } from 'payload'
-import { asc, desc } from 'drizzle-orm'
+import { asc, desc, or } from 'drizzle-orm'
import type { DrizzleAdapter, GenericColumn } from '../types.js'
import type { BuildQueryJoinAliases, BuildQueryResult } from './buildQuery.js'
@@ -16,6 +16,7 @@ type Args = {
joins: BuildQueryJoinAliases
locale?: string
parentIsLocalized: boolean
+ rawSort?: SQL
selectFields: Record
sort?: Sort
tableName: string
@@ -31,6 +32,7 @@ export const buildOrderBy = ({
joins,
locale,
parentIsLocalized,
+ rawSort,
selectFields,
sort,
tableName,
@@ -74,12 +76,18 @@ export const buildOrderBy = ({
value: sortProperty,
})
if (sortTable?.[sortTableColumnName]) {
+ let order = sortDirection === 'asc' ? asc : desc
+
+ if (rawSort) {
+ order = () => rawSort
+ }
+
orderBy.push({
column:
aliasTable && tableName === getNameFromDrizzleTable(sortTable)
? aliasTable[sortTableColumnName]
: sortTable[sortTableColumnName],
- order: sortDirection === 'asc' ? asc : desc,
+ order,
})
selectFields[sortTableColumnName] = sortTable[sortTableColumnName]
diff --git a/packages/drizzle/src/queries/buildQuery.ts b/packages/drizzle/src/queries/buildQuery.ts
index 9b5326b9cc..b91869c46f 100644
--- a/packages/drizzle/src/queries/buildQuery.ts
+++ b/packages/drizzle/src/queries/buildQuery.ts
@@ -79,6 +79,7 @@ const buildQuery = function buildQuery({
joins,
locale,
parentIsLocalized,
+ rawSort: context.rawSort,
selectFields,
sort: context.sort,
tableName,
diff --git a/packages/drizzle/src/queries/parseParams.ts b/packages/drizzle/src/queries/parseParams.ts
index 90bba4136e..8ca7503669 100644
--- a/packages/drizzle/src/queries/parseParams.ts
+++ b/packages/drizzle/src/queries/parseParams.ts
@@ -14,7 +14,7 @@ import { buildAndOrConditions } from './buildAndOrConditions.js'
import { getTableColumnFromPath } from './getTableColumnFromPath.js'
import { sanitizeQueryValue } from './sanitizeQueryValue.js'
-export type QueryContext = { sort: Sort }
+export type QueryContext = { rawSort?: SQL; sort: Sort }
type Args = {
adapter: DrizzleAdapter
@@ -348,6 +348,7 @@ export function parseParams({
}
if (geoConstraints.length) {
context.sort = relationOrPath
+ context.rawSort = sql`${table[columnName]} <-> ST_SetSRID(ST_MakePoint(${lng}, ${lat}), 4326)`
constraints.push(and(...geoConstraints))
}
break
diff --git a/packages/drizzle/src/schema/traverseFields.ts b/packages/drizzle/src/schema/traverseFields.ts
index a10c09e704..79136ec2f0 100644
--- a/packages/drizzle/src/schema/traverseFields.ts
+++ b/packages/drizzle/src/schema/traverseFields.ts
@@ -23,6 +23,7 @@ import type {
import { createTableName } from '../createTableName.js'
import { buildIndexName } from '../utilities/buildIndexName.js'
+import { getArrayRelationName } from '../utilities/getArrayRelationName.js'
import { hasLocalesTable } from '../utilities/hasLocalesTable.js'
import { validateExistingBlockIsIdentical } from '../utilities/validateExistingBlockIsIdentical.js'
import { buildTable } from './build.js'
@@ -288,7 +289,11 @@ export const traverseFields = ({
}
}
- const relationName = field.dbName ? `_${arrayTableName}` : fieldName
+ const relationName = getArrayRelationName({
+ field,
+ path: fieldName,
+ tableName: arrayTableName,
+ })
relationsToBuild.set(relationName, {
type: 'many',
diff --git a/packages/drizzle/src/transform/read/traverseFields.ts b/packages/drizzle/src/transform/read/traverseFields.ts
index 43fbf0539c..b063d7f65c 100644
--- a/packages/drizzle/src/transform/read/traverseFields.ts
+++ b/packages/drizzle/src/transform/read/traverseFields.ts
@@ -6,6 +6,7 @@ import toSnakeCase from 'to-snake-case'
import type { DrizzleAdapter } from '../../types.js'
import type { BlocksMap } from '../../utilities/createBlocksMap.js'
+import { getArrayRelationName } from '../../utilities/getArrayRelationName.js'
import { transformHasManyNumber } from './hasManyNumber.js'
import { transformHasManyText } from './hasManyText.js'
import { transformRelationship } from './relationship.js'
@@ -121,9 +122,7 @@ export const traverseFields = >({
`${currentTableName}_${tablePath}${toSnakeCase(field.name)}`,
)
- if (field.dbName) {
- fieldData = table[`_${arrayTableName}`]
- }
+ fieldData = table[getArrayRelationName({ field, path: fieldName, tableName: arrayTableName })]
if (Array.isArray(fieldData)) {
if (isLocalized) {
diff --git a/packages/drizzle/src/utilities/createSchemaGenerator.ts b/packages/drizzle/src/utilities/createSchemaGenerator.ts
index 25bf2f5dac..b979460b26 100644
--- a/packages/drizzle/src/utilities/createSchemaGenerator.ts
+++ b/packages/drizzle/src/utilities/createSchemaGenerator.ts
@@ -267,8 +267,11 @@ declare module '${this.packageName}' {
*/
`
+ const importTypes = `import type {} from '${this.packageName}'`
+
let code = [
warning,
+ importTypes,
...importDeclarationsSanitized,
schemaDeclaration,
...enumDeclarations,
diff --git a/packages/drizzle/src/utilities/getArrayRelationName.ts b/packages/drizzle/src/utilities/getArrayRelationName.ts
new file mode 100644
index 0000000000..4127d26a4a
--- /dev/null
+++ b/packages/drizzle/src/utilities/getArrayRelationName.ts
@@ -0,0 +1,17 @@
+import type { ArrayField } from 'payload'
+
+export const getArrayRelationName = ({
+ field,
+ path,
+ tableName,
+}: {
+ field: ArrayField
+ path: string
+ tableName: string
+}) => {
+ if (field.dbName && path.length > 63) {
+ return `_${tableName}`
+ }
+
+ return path
+}
diff --git a/packages/drizzle/src/utilities/pushDevSchema.ts b/packages/drizzle/src/utilities/pushDevSchema.ts
index 46f2495aa0..0406846e23 100644
--- a/packages/drizzle/src/utilities/pushDevSchema.ts
+++ b/packages/drizzle/src/utilities/pushDevSchema.ts
@@ -1,4 +1,4 @@
-import { deepStrictEqual } from 'assert'
+import { dequal } from 'dequal'
import prompts from 'prompts'
import type { BasePostgresAdapter } from '../postgres/types.js'
@@ -23,18 +23,18 @@ export const pushDevSchema = async (adapter: DrizzleAdapter) => {
const localeCodes =
adapter.payload.config.localization && adapter.payload.config.localization.localeCodes
- try {
- deepStrictEqual(previousSchema, {
- localeCodes,
- rawTables: adapter.rawTables,
- })
+ const equal = dequal(previousSchema, {
+ localeCodes,
+ rawTables: adapter.rawTables,
+ })
+ if (equal) {
if (adapter.logger) {
adapter.payload.logger.info('No changes detected in schema, skipping schema push.')
}
return
- } catch {
+ } else {
previousSchema.localeCodes = localeCodes
previousSchema.rawTables = adapter.rawTables
}
diff --git a/packages/email-nodemailer/package.json b/packages/email-nodemailer/package.json
index 0d36ea0b00..4bf652f73e 100644
--- a/packages/email-nodemailer/package.json
+++ b/packages/email-nodemailer/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/email-resend/package.json b/packages/email-resend/package.json
index c986fa70d4..04a5fb9872 100644
--- a/packages/email-resend/package.json
+++ b/packages/email-resend/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-resend",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/graphql/package.json b/packages/graphql/package.json
index 10f27f1383..45228a4a30 100644
--- a/packages/graphql/package.json
+++ b/packages/graphql/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
- "version": "3.36.1",
+ "version": "3.38.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
diff --git a/packages/graphql/src/schema/buildMutationInputType.ts b/packages/graphql/src/schema/buildMutationInputType.ts
index 44e627b8ab..448ac80570 100644
--- a/packages/graphql/src/schema/buildMutationInputType.ts
+++ b/packages/graphql/src/schema/buildMutationInputType.ts
@@ -145,27 +145,37 @@ export function buildMutationInputType({
},
}),
group: (inputObjectTypeConfig: InputObjectTypeConfig, field: GroupField) => {
- const requiresAtLeastOneField = groupOrTabHasRequiredSubfield(field)
- const fullName = combineParentName(parentName, toWords(field.name, true))
- let type: GraphQLType = buildMutationInputType({
- name: fullName,
- config,
- fields: field.fields,
- graphqlResult,
- parentIsLocalized: parentIsLocalized || field.localized,
- parentName: fullName,
- })
+ if (fieldAffectsData(field)) {
+ const requiresAtLeastOneField = groupOrTabHasRequiredSubfield(field)
+ const fullName = combineParentName(parentName, toWords(field.name, true))
+ let type: GraphQLType = buildMutationInputType({
+ name: fullName,
+ config,
+ fields: field.fields,
+ graphqlResult,
+ parentIsLocalized: parentIsLocalized || field.localized,
+ parentName: fullName,
+ })
- if (!type) {
- return inputObjectTypeConfig
- }
+ if (!type) {
+ return inputObjectTypeConfig
+ }
- if (requiresAtLeastOneField) {
- type = new GraphQLNonNull(type)
- }
- return {
- ...inputObjectTypeConfig,
- [formatName(field.name)]: { type },
+ if (requiresAtLeastOneField) {
+ type = new GraphQLNonNull(type)
+ }
+ return {
+ ...inputObjectTypeConfig,
+ [formatName(field.name)]: { type },
+ }
+ } else {
+ return field.fields.reduce((acc, subField: CollapsibleField) => {
+ const addSubField = fieldToSchemaMap[subField.type]
+ if (addSubField) {
+ return addSubField(acc, subField)
+ }
+ return acc
+ }, inputObjectTypeConfig)
}
},
json: (inputObjectTypeConfig: InputObjectTypeConfig, field: JSONField) => ({
diff --git a/packages/graphql/src/schema/buildPaginatedListType.ts b/packages/graphql/src/schema/buildPaginatedListType.ts
index 343fd28cba..2a8a64c5d1 100644
--- a/packages/graphql/src/schema/buildPaginatedListType.ts
+++ b/packages/graphql/src/schema/buildPaginatedListType.ts
@@ -10,11 +10,11 @@ export const buildPaginatedListType = (name, docType) =>
hasNextPage: { type: new GraphQLNonNull(GraphQLBoolean) },
hasPrevPage: { type: new GraphQLNonNull(GraphQLBoolean) },
limit: { type: new GraphQLNonNull(GraphQLInt) },
- nextPage: { type: new GraphQLNonNull(GraphQLInt) },
+ nextPage: { type: GraphQLInt },
offset: { type: GraphQLInt },
page: { type: new GraphQLNonNull(GraphQLInt) },
pagingCounter: { type: new GraphQLNonNull(GraphQLInt) },
- prevPage: { type: new GraphQLNonNull(GraphQLInt) },
+ prevPage: { type: GraphQLInt },
totalDocs: { type: new GraphQLNonNull(GraphQLInt) },
totalPages: { type: new GraphQLNonNull(GraphQLInt) },
},
diff --git a/packages/graphql/src/schema/fieldToSchemaMap.ts b/packages/graphql/src/schema/fieldToSchemaMap.ts
index fc5750add9..571bc61585 100644
--- a/packages/graphql/src/schema/fieldToSchemaMap.ts
+++ b/packages/graphql/src/schema/fieldToSchemaMap.ts
@@ -41,7 +41,7 @@ import {
} from 'graphql'
import { DateTimeResolver, EmailAddressResolver } from 'graphql-scalars'
import { combineQueries, createDataloaderCacheKey, MissingEditorProp, toWords } from 'payload'
-import { tabHasName } from 'payload/shared'
+import { fieldAffectsData, tabHasName } from 'payload/shared'
import type { Context } from '../resolvers/types.js'
@@ -302,44 +302,64 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
field,
forceNullable,
graphqlResult,
+ newlyCreatedBlockType,
objectTypeConfig,
parentIsLocalized,
parentName,
}) => {
- const interfaceName =
- field?.interfaceName || combineParentName(parentName, toWords(field.name, true))
+ if (fieldAffectsData(field)) {
+ const interfaceName =
+ field?.interfaceName || combineParentName(parentName, toWords(field.name, true))
- if (!graphqlResult.types.groupTypes[interfaceName]) {
- const objectType = buildObjectType({
- name: interfaceName,
- config,
- fields: field.fields,
- forceNullable: isFieldNullable({ field, forceNullable, parentIsLocalized }),
- graphqlResult,
- parentIsLocalized: field.localized || parentIsLocalized,
- parentName: interfaceName,
- })
+ if (!graphqlResult.types.groupTypes[interfaceName]) {
+ const objectType = buildObjectType({
+ name: interfaceName,
+ config,
+ fields: field.fields,
+ forceNullable: isFieldNullable({ field, forceNullable, parentIsLocalized }),
+ graphqlResult,
+ parentIsLocalized: field.localized || parentIsLocalized,
+ parentName: interfaceName,
+ })
- if (Object.keys(objectType.getFields()).length) {
- graphqlResult.types.groupTypes[interfaceName] = objectType
+ if (Object.keys(objectType.getFields()).length) {
+ graphqlResult.types.groupTypes[interfaceName] = objectType
+ }
}
- }
- if (!graphqlResult.types.groupTypes[interfaceName]) {
- return objectTypeConfig
- }
+ if (!graphqlResult.types.groupTypes[interfaceName]) {
+ return objectTypeConfig
+ }
- return {
- ...objectTypeConfig,
- [formatName(field.name)]: {
- type: graphqlResult.types.groupTypes[interfaceName],
- resolve: (parent, args, context: Context) => {
- return {
- ...parent[field.name],
- _id: parent._id ?? parent.id,
- }
+ return {
+ ...objectTypeConfig,
+ [formatName(field.name)]: {
+ type: graphqlResult.types.groupTypes[interfaceName],
+ resolve: (parent, args, context: Context) => {
+ return {
+ ...parent[field.name],
+ _id: parent._id ?? parent.id,
+ }
+ },
},
- },
+ }
+ } else {
+ return field.fields.reduce((objectTypeConfigWithCollapsibleFields, subField) => {
+ const addSubField: GenericFieldToSchemaMap = fieldToSchemaMap[subField.type]
+ if (addSubField) {
+ return addSubField({
+ config,
+ field: subField,
+ forceNullable,
+ graphqlResult,
+ newlyCreatedBlockType,
+ objectTypeConfig: objectTypeConfigWithCollapsibleFields,
+ parentIsLocalized,
+ parentName,
+ })
+ }
+ return objectTypeConfigWithCollapsibleFields
+ }, objectTypeConfig)
}
},
join: ({ collectionSlug, field, graphqlResult, objectTypeConfig, parentName }) => {
diff --git a/packages/live-preview-react/package.json b/packages/live-preview-react/package.json
index 70fb80fcb7..24991df8a4 100644
--- a/packages/live-preview-react/package.json
+++ b/packages/live-preview-react/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/live-preview-vue/package.json b/packages/live-preview-vue/package.json
index 219c04d332..d81abf67a2 100644
--- a/packages/live-preview-vue/package.json
+++ b/packages/live-preview-vue/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "The official Vue SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/live-preview/package.json b/packages/live-preview/package.json
index 8caac4e6aa..394f1be881 100644
--- a/packages/live-preview/package.json
+++ b/packages/live-preview/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/next/package.json b/packages/next/package.json
index 9102fc40ca..fd88d6f802 100644
--- a/packages/next/package.json
+++ b/packages/next/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
- "version": "3.36.1",
+ "version": "3.38.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
diff --git a/packages/next/src/elements/Nav/getNavPrefs.ts b/packages/next/src/elements/Nav/getNavPrefs.ts
index a64e24227e..0a5517eb6c 100644
--- a/packages/next/src/elements/Nav/getNavPrefs.ts
+++ b/packages/next/src/elements/Nav/getNavPrefs.ts
@@ -1,41 +1,36 @@
-import type { DefaultDocumentIDType, NavPreferences, Payload, User } from 'payload'
+import type { NavPreferences, PayloadRequest } from 'payload'
import { cache } from 'react'
-export const getNavPrefs = cache(
- async (
- payload: Payload,
- userID: DefaultDocumentIDType,
- userSlug: string,
- ): Promise => {
- return userSlug
- ? await payload
- .find({
- collection: 'payload-preferences',
- depth: 0,
- limit: 1,
- pagination: false,
- where: {
- and: [
- {
- key: {
- equals: 'nav',
- },
+export const getNavPrefs = cache(async (req: PayloadRequest): Promise => {
+ return req?.user?.collection
+ ? await req.payload
+ .find({
+ collection: 'payload-preferences',
+ depth: 0,
+ limit: 1,
+ pagination: false,
+ req,
+ where: {
+ and: [
+ {
+ key: {
+ equals: 'nav',
},
- {
- 'user.relationTo': {
- equals: userSlug,
- },
+ },
+ {
+ 'user.relationTo': {
+ equals: req.user.collection,
},
- {
- 'user.value': {
- equals: userID,
- },
+ },
+ {
+ 'user.value': {
+ equals: req?.user?.id,
},
- ],
- },
- })
- ?.then((res) => res?.docs?.[0]?.value)
- : null
- },
-)
+ },
+ ],
+ },
+ })
+ ?.then((res) => res?.docs?.[0]?.value)
+ : null
+})
diff --git a/packages/next/src/elements/Nav/index.tsx b/packages/next/src/elements/Nav/index.tsx
index 1ee12bc2e1..c7aabc3aec 100644
--- a/packages/next/src/elements/Nav/index.tsx
+++ b/packages/next/src/elements/Nav/index.tsx
@@ -1,5 +1,5 @@
import type { EntityToGroup } from '@payloadcms/ui/shared'
-import type { ServerProps } from 'payload'
+import type { PayloadRequest, ServerProps } from 'payload'
import { Logout } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
@@ -15,7 +15,9 @@ const baseClass = 'nav'
import { getNavPrefs } from './getNavPrefs.js'
import { DefaultNavClient } from './index.client.js'
-export type NavProps = ServerProps
+export type NavProps = {
+ req?: PayloadRequest
+} & ServerProps
export const DefaultNav: React.FC = async (props) => {
const {
@@ -25,6 +27,7 @@ export const DefaultNav: React.FC = async (props) => {
params,
payload,
permissions,
+ req,
searchParams,
user,
viewType,
@@ -68,7 +71,7 @@ export const DefaultNav: React.FC = async (props) => {
i18n,
)
- const navPreferences = await getNavPrefs(payload, user?.id, user?.collection)
+ const navPreferences = await getNavPrefs(req)
const LogoutComponent = RenderServerComponent({
clientProps: {
diff --git a/packages/next/src/layouts/Root/index.tsx b/packages/next/src/layouts/Root/index.tsx
index f0c8c31526..ac2319aabb 100644
--- a/packages/next/src/layouts/Root/index.tsx
+++ b/packages/next/src/layouts/Root/index.tsx
@@ -79,7 +79,7 @@ export const RootLayout = async ({
})
}
- const navPrefs = await getNavPrefs(req.payload, req.user?.id, req.user?.collection)
+ const navPrefs = await getNavPrefs(req)
const clientConfig = getClientConfig({
config,
diff --git a/packages/next/src/templates/Default/index.tsx b/packages/next/src/templates/Default/index.tsx
index a0418a3607..431d5f32da 100644
--- a/packages/next/src/templates/Default/index.tsx
+++ b/packages/next/src/templates/Default/index.tsx
@@ -1,6 +1,7 @@
import type {
CustomComponent,
DocumentSubViewTypes,
+ PayloadRequest,
ServerProps,
ViewTypes,
VisibleEntities,
@@ -32,6 +33,7 @@ export type DefaultTemplateProps = {
docID?: number | string
documentSubViewType?: DocumentSubViewTypes
globalSlug?: string
+ req?: PayloadRequest
viewActions?: CustomComponent[]
viewType?: ViewTypes
visibleEntities: VisibleEntities
@@ -49,6 +51,7 @@ export const DefaultTemplate: React.FC = ({
params,
payload,
permissions,
+ req,
searchParams,
user,
viewActions,
@@ -84,6 +87,7 @@ export const DefaultTemplate: React.FC = ({
params,
payload,
permissions,
+ req,
searchParams,
user,
}),
@@ -98,6 +102,7 @@ export const DefaultTemplate: React.FC = ({
globalSlug,
collectionSlug,
docID,
+ req,
],
)
diff --git a/packages/next/src/views/Document/getDocumentData.ts b/packages/next/src/views/Document/getDocumentData.ts
index f1ca4226ca..5200813b6f 100644
--- a/packages/next/src/views/Document/getDocumentData.ts
+++ b/packages/next/src/views/Document/getDocumentData.ts
@@ -29,6 +29,7 @@ export const getDocumentData = async ({
}: Args): Promise | TypeWithID> => {
const id = sanitizeID(idArg)
let resolvedData: Record | TypeWithID = null
+ const { transactionID, ...rest } = req
try {
if (collectionSlug && id) {
@@ -41,9 +42,7 @@ export const getDocumentData = async ({
locale: locale?.code,
overrideAccess: false,
req: {
- query: req?.query,
- search: req?.search,
- searchParams: req?.searchParams,
+ ...rest,
},
user,
})
@@ -58,9 +57,7 @@ export const getDocumentData = async ({
locale: locale?.code,
overrideAccess: false,
req: {
- query: req?.query,
- search: req?.search,
- searchParams: req?.searchParams,
+ ...rest,
},
user,
})
diff --git a/packages/next/src/views/Root/index.tsx b/packages/next/src/views/Root/index.tsx
index a85e719bbf..ee7c5067ee 100644
--- a/packages/next/src/views/Root/index.tsx
+++ b/packages/next/src/views/Root/index.tsx
@@ -173,6 +173,7 @@ export const RootPage = async ({
params={params}
payload={initPageResult?.req.payload}
permissions={initPageResult?.permissions}
+ req={initPageResult?.req}
searchParams={searchParams}
user={initPageResult?.req.user}
viewActions={serverProps.viewActions}
diff --git a/packages/next/src/withPayload.js b/packages/next/src/withPayload.js
index d72690c55f..1dd1f36b8d 100644
--- a/packages/next/src/withPayload.js
+++ b/packages/next/src/withPayload.js
@@ -132,6 +132,7 @@ export const withPayload = (nextConfig = {}, options = {}) => {
'drizzle-kit/api',
'sharp',
'libsql',
+ 'require-in-the-middle',
],
ignoreWarnings: [
...(incomingWebpackConfig?.ignoreWarnings || []),
diff --git a/packages/payload-cloud/package.json b/packages/payload-cloud/package.json
index 9e7ce8217e..283dddb68e 100644
--- a/packages/payload-cloud/package.json
+++ b/packages/payload-cloud/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/payload-cloud",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/payload-cloud/src/plugin.ts b/packages/payload-cloud/src/plugin.ts
index ace1336907..c72236ddaa 100644
--- a/packages/payload-cloud/src/plugin.ts
+++ b/packages/payload-cloud/src/plugin.ts
@@ -16,6 +16,14 @@ export const generateRandomString = (): string => {
return Array.from({ length: 24 }, () => chars[Math.floor(Math.random() * chars.length)]).join('')
}
+const DEFAULT_CRON = '* * * * *'
+const DEFAULT_LIMIT = 10
+const DEFAULT_CRON_JOB = {
+ cron: DEFAULT_CRON,
+ limit: DEFAULT_LIMIT,
+ queue: 'default',
+}
+
export const payloadCloudPlugin =
(pluginOptions?: PluginOptions) =>
async (incomingConfig: Config): Promise => {
@@ -100,15 +108,6 @@ export const payloadCloudPlugin =
}
// We make sure to only run cronjobs on one instance using a instance identifier stored in a global.
-
- const DEFAULT_CRON = '* * * * *'
- const DEFAULT_LIMIT = 10
- const DEFAULT_CRON_JOB = {
- cron: DEFAULT_CRON,
- limit: DEFAULT_LIMIT,
- queue: 'default',
- }
-
config.globals = [
...(config.globals || []),
{
@@ -126,13 +125,13 @@ export const payloadCloudPlugin =
},
]
- if (pluginOptions?.enableAutoRun === false || !config.jobs) {
+ if (pluginOptions?.enableAutoRun === false) {
return config
}
- const oldAutoRunCopy = config.jobs.autoRun ?? []
+ const oldAutoRunCopy = config.jobs?.autoRun ?? []
- const hasExistingAutorun = Boolean(config.jobs.autoRun)
+ const hasExistingAutorun = Boolean(config.jobs?.autoRun)
const newShouldAutoRun = async (payload: Payload) => {
if (process.env.PAYLOAD_CLOUD_JOBS_INSTANCE) {
@@ -150,8 +149,8 @@ export const payloadCloudPlugin =
return false
}
- if (!config.jobs.shouldAutoRun) {
- config.jobs.shouldAutoRun = newShouldAutoRun
+ if (!config.jobs?.shouldAutoRun) {
+ ;(config.jobs ??= {}).shouldAutoRun = newShouldAutoRun
}
const newAutoRun = async (payload: Payload) => {
diff --git a/packages/payload/package.json b/packages/payload/package.json
index 0b62938b55..55d139aefc 100644
--- a/packages/payload/package.json
+++ b/packages/payload/package.json
@@ -1,6 +1,6 @@
{
"name": "payload",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",
diff --git a/packages/payload/src/admin/RichText.ts b/packages/payload/src/admin/RichText.ts
index 0e2d0501c5..5c9e89d72a 100644
--- a/packages/payload/src/admin/RichText.ts
+++ b/packages/payload/src/admin/RichText.ts
@@ -65,6 +65,7 @@ export type AfterChangeRichTextHookArgs<
/** The previous value of the field, before changes */
previousValue?: TValue
}
+
export type BeforeValidateRichTextHookArgs<
TData extends TypeWithID = any,
TValue = any,
@@ -102,11 +103,11 @@ export type BeforeChangeRichTextHookArgs<
mergeLocaleActions?: (() => Promise | void)[]
/** A string relating to which operation the field type is currently executing within. */
operation?: 'create' | 'delete' | 'read' | 'update'
+ overrideAccess: boolean
/** The sibling data of the document before changes being applied. */
previousSiblingDoc?: TData
/** The previous value of the field, before changes */
previousValue?: TValue
-
/**
* The original siblingData with locales (not modified by any hooks).
*/
@@ -190,6 +191,7 @@ export type RichTextHooks = {
beforeChange?: BeforeChangeRichTextHook[]
beforeValidate?: BeforeValidateRichTextHook[]
}
+
type RichTextAdapterBase<
Value extends object = object,
AdapterProps = any,
diff --git a/packages/payload/src/admin/forms/Field.ts b/packages/payload/src/admin/forms/Field.ts
index d6fd4d626a..13debab2b5 100644
--- a/packages/payload/src/admin/forms/Field.ts
+++ b/packages/payload/src/admin/forms/Field.ts
@@ -91,6 +91,7 @@ export type ServerComponentProps = {
req: PayloadRequest
siblingData: Data
user: User
+ value?: unknown
}
export type ClientFieldBase<
diff --git a/packages/payload/src/auth/getFieldsToSign.ts b/packages/payload/src/auth/getFieldsToSign.ts
index f6a2b774f9..c66bf40ca3 100644
--- a/packages/payload/src/auth/getFieldsToSign.ts
+++ b/packages/payload/src/auth/getFieldsToSign.ts
@@ -28,25 +28,35 @@ const traverseFields = ({
break
}
case 'group': {
- let targetResult
- if (typeof field.saveToJWT === 'string') {
- targetResult = field.saveToJWT
- result[field.saveToJWT] = data[field.name]
- } else if (field.saveToJWT) {
- targetResult = field.name
- result[field.name] = data[field.name]
+ if (fieldAffectsData(field)) {
+ let targetResult
+ if (typeof field.saveToJWT === 'string') {
+ targetResult = field.saveToJWT
+ result[field.saveToJWT] = data[field.name]
+ } else if (field.saveToJWT) {
+ targetResult = field.name
+ result[field.name] = data[field.name]
+ }
+ const groupData: Record = data[field.name] as Record
+ const groupResult = (targetResult ? result[targetResult] : result) as Record<
+ string,
+ unknown
+ >
+ traverseFields({
+ data: groupData,
+ fields: field.fields,
+ result: groupResult,
+ })
+ break
+ } else {
+ traverseFields({
+ data,
+ fields: field.fields,
+ result,
+ })
+
+ break
}
- const groupData: Record = data[field.name] as Record
- const groupResult = (targetResult ? result[targetResult] : result) as Record<
- string,
- unknown
- >
- traverseFields({
- data: groupData,
- fields: field.fields,
- result: groupResult,
- })
- break
}
case 'tab': {
if (tabHasName(field)) {
diff --git a/packages/payload/src/auth/isLocked.ts b/packages/payload/src/auth/isLocked.ts
deleted file mode 100644
index 95efbea80f..0000000000
--- a/packages/payload/src/auth/isLocked.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-const isLocked = (date: number): boolean => {
- if (!date) {
- return false
- }
- return date > Date.now()
-}
-export default isLocked
diff --git a/packages/payload/src/auth/isUserLocked.ts b/packages/payload/src/auth/isUserLocked.ts
new file mode 100644
index 0000000000..a1ea9c2bf7
--- /dev/null
+++ b/packages/payload/src/auth/isUserLocked.ts
@@ -0,0 +1,6 @@
+export const isUserLocked = (date: number): boolean => {
+ if (!date) {
+ return false
+ }
+ return date > Date.now()
+}
diff --git a/packages/payload/src/auth/operations/login.ts b/packages/payload/src/auth/operations/login.ts
index d133ad0e18..5bd02efd2f 100644
--- a/packages/payload/src/auth/operations/login.ts
+++ b/packages/payload/src/auth/operations/login.ts
@@ -3,6 +3,7 @@ import type {
AuthOperationsFromCollectionSlug,
Collection,
DataFromCollectionSlug,
+ SanitizedCollectionConfig,
} from '../../collections/config/types.js'
import type { CollectionSlug } from '../../index.js'
import type { PayloadRequest, Where } from '../../types/index.js'
@@ -21,7 +22,7 @@ import { killTransaction } from '../../utilities/killTransaction.js'
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js'
import { getFieldsToSign } from '../getFieldsToSign.js'
import { getLoginOptions } from '../getLoginOptions.js'
-import isLocked from '../isLocked.js'
+import { isUserLocked } from '../isUserLocked.js'
import { jwtSign } from '../jwt.js'
import { authenticateLocalStrategy } from '../strategies/local/authenticate.js'
import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts.js'
@@ -42,6 +43,32 @@ export type Arguments = {
showHiddenFields?: boolean
}
+type CheckLoginPermissionArgs = {
+ collection: SanitizedCollectionConfig
+ loggingInWithUsername?: boolean
+ req: PayloadRequest
+ user: any
+}
+
+export const checkLoginPermission = ({
+ collection,
+ loggingInWithUsername,
+ req,
+ user,
+}: CheckLoginPermissionArgs) => {
+ if (!user) {
+ throw new AuthenticationError(req.t, Boolean(loggingInWithUsername))
+ }
+
+ if (collection.auth.verify && user._verified === false) {
+ throw new UnverifiedEmail({ t: req.t })
+ }
+
+ if (isUserLocked(new Date(user.lockUntil).getTime())) {
+ throw new LockedAuth(req.t)
+ }
+}
+
export const loginOperation = async (
incomingArgs: Arguments,
): Promise<{ user: DataFromCollectionSlug } & Result> => {
@@ -184,21 +211,16 @@ export const loginOperation = async (
where: whereConstraint,
})
- if (!user) {
- throw new AuthenticationError(req.t, Boolean(canLoginWithUsername && sanitizedUsername))
- }
-
- if (args.collection.config.auth.verify && user._verified === false) {
- throw new UnverifiedEmail({ t: req.t })
- }
+ checkLoginPermission({
+ collection: collectionConfig,
+ loggingInWithUsername: Boolean(canLoginWithUsername && sanitizedUsername),
+ req,
+ user,
+ })
user.collection = collectionConfig.slug
user._strategy = 'local-jwt'
- if (isLocked(new Date(user.lockUntil).getTime())) {
- throw new LockedAuth(req.t)
- }
-
const authResult = await authenticateLocalStrategy({ doc: user, password })
user = sanitizeInternalFields(user)
diff --git a/packages/payload/src/collections/config/reservedFieldNames.ts b/packages/payload/src/collections/config/reservedFieldNames.ts
deleted file mode 100644
index 199571dae1..0000000000
--- a/packages/payload/src/collections/config/reservedFieldNames.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-// @ts-strict-ignore
-import type { Field } from '../../fields/config/types.js'
-import type { CollectionConfig } from '../../index.js'
-
-import { ReservedFieldName } from '../../errors/ReservedFieldName.js'
-import { fieldAffectsData } from '../../fields/config/types.js'
-
-// Note for future reference: We've slimmed down the reserved field names but left them in here for reference in case it's needed in the future.
-
-/**
- * Reserved field names for collections with auth config enabled
- */
-const reservedBaseAuthFieldNames = [
- /* 'email',
- 'resetPasswordToken',
- 'resetPasswordExpiration', */
- 'salt',
- 'hash',
-]
-/**
- * Reserved field names for auth collections with verify: true
- */
-const reservedVerifyFieldNames = [
- /* '_verified', '_verificationToken' */
-]
-/**
- * Reserved field names for auth collections with useApiKey: true
- */
-const reservedAPIKeyFieldNames = [
- /* 'enableAPIKey', 'apiKeyIndex', 'apiKey' */
-]
-
-/**
- * Reserved field names for collections with upload config enabled
- */
-const reservedBaseUploadFieldNames = [
- 'file',
- /* 'mimeType',
- 'thumbnailURL',
- 'width',
- 'height',
- 'filesize',
- 'filename',
- 'url',
- 'focalX',
- 'focalY',
- 'sizes', */
-]
-
-/**
- * Reserved field names for collections with versions enabled
- */
-const reservedVersionsFieldNames = [
- /* '__v', '_status' */
-]
-
-/**
- * Sanitize fields for collections with auth config enabled.
- *
- * Should run on top level fields only.
- */
-export const sanitizeAuthFields = (fields: Field[], config: CollectionConfig) => {
- for (let i = 0; i < fields.length; i++) {
- const field = fields[i]
-
- if (fieldAffectsData(field) && field.name) {
- if (config.auth && typeof config.auth === 'object' && !config.auth.disableLocalStrategy) {
- const auth = config.auth
-
- if (reservedBaseAuthFieldNames.includes(field.name)) {
- throw new ReservedFieldName(field, field.name)
- }
-
- if (auth.verify) {
- if (reservedAPIKeyFieldNames.includes(field.name)) {
- throw new ReservedFieldName(field, field.name)
- }
- }
-
- /* if (auth.maxLoginAttempts) {
- if (field.name === 'loginAttempts' || field.name === 'lockUntil') {
- throw new ReservedFieldName(field, field.name)
- }
- } */
-
- /* if (auth.loginWithUsername) {
- if (field.name === 'username') {
- throw new ReservedFieldName(field, field.name)
- }
- } */
-
- if (auth.verify) {
- if (reservedVerifyFieldNames.includes(field.name)) {
- throw new ReservedFieldName(field, field.name)
- }
- }
- }
- }
-
- // Handle tabs without a name
- if (field.type === 'tabs') {
- for (let j = 0; j < field.tabs.length; j++) {
- const tab = field.tabs[j]
-
- if (!('name' in tab)) {
- sanitizeAuthFields(tab.fields, config)
- }
- }
- }
-
- // Handle presentational fields like rows and collapsibles
- if (!fieldAffectsData(field) && 'fields' in field && field.fields) {
- sanitizeAuthFields(field.fields, config)
- }
- }
-}
-
-/**
- * Sanitize fields for collections with upload config enabled.
- *
- * Should run on top level fields only.
- */
-export const sanitizeUploadFields = (fields: Field[], config: CollectionConfig) => {
- if (config.upload && typeof config.upload === 'object') {
- for (let i = 0; i < fields.length; i++) {
- const field = fields[i]
-
- if (fieldAffectsData(field) && field.name) {
- if (reservedBaseUploadFieldNames.includes(field.name)) {
- throw new ReservedFieldName(field, field.name)
- }
- }
-
- // Handle tabs without a name
- if (field.type === 'tabs') {
- for (let j = 0; j < field.tabs.length; j++) {
- const tab = field.tabs[j]
-
- if (!('name' in tab)) {
- sanitizeUploadFields(tab.fields, config)
- }
- }
- }
-
- // Handle presentational fields like rows and collapsibles
- if (!fieldAffectsData(field) && 'fields' in field && field.fields) {
- sanitizeUploadFields(field.fields, config)
- }
- }
- }
-}
diff --git a/packages/payload/src/collections/config/sanitize.ts b/packages/payload/src/collections/config/sanitize.ts
index 31198bc80d..fd29217f2b 100644
--- a/packages/payload/src/collections/config/sanitize.ts
+++ b/packages/payload/src/collections/config/sanitize.ts
@@ -26,7 +26,6 @@ import {
addDefaultsToCollectionConfig,
addDefaultsToLoginWithUsernameConfig,
} from './defaults.js'
-import { sanitizeAuthFields, sanitizeUploadFields } from './reservedFieldNames.js'
import { sanitizeCompoundIndexes } from './sanitizeCompoundIndexes.js'
import { validateUseAsTitle } from './useAsTitle.js'
@@ -43,7 +42,9 @@ export const sanitizeCollection = async (
if (collection._sanitized) {
return collection as SanitizedCollectionConfig
}
+
collection._sanitized = true
+
// /////////////////////////////////
// Make copy of collection config
// /////////////////////////////////
@@ -57,7 +58,9 @@ export const sanitizeCollection = async (
const validRelationships = _validRelationships ?? config.collections.map((c) => c.slug) ?? []
const joins: SanitizedJoins = {}
+
const polymorphicJoins: SanitizedJoin[] = []
+
sanitized.fields = await sanitizeFields({
collectionConfig: sanitized,
config,
@@ -96,17 +99,21 @@ export const sanitizeCollection = async (
// add default timestamps fields only as needed
let hasUpdatedAt: boolean | null = null
let hasCreatedAt: boolean | null = null
+
sanitized.fields.some((field) => {
if (fieldAffectsData(field)) {
if (field.name === 'updatedAt') {
hasUpdatedAt = true
}
+
if (field.name === 'createdAt') {
hasCreatedAt = true
}
}
+
return hasCreatedAt && hasUpdatedAt
})
+
if (!hasUpdatedAt) {
sanitized.fields.push({
name: 'updatedAt',
@@ -119,6 +126,7 @@ export const sanitizeCollection = async (
label: ({ t }) => t('general:updatedAt'),
})
}
+
if (!hasCreatedAt) {
sanitized.fields.push({
name: 'createdAt',
@@ -175,9 +183,6 @@ export const sanitizeCollection = async (
sanitized.upload = {}
}
- // sanitize fields for reserved names
- sanitizeUploadFields(sanitized.fields, sanitized)
-
sanitized.upload.cacheTags = sanitized.upload?.cacheTags ?? true
sanitized.upload.bulkUpload = sanitized.upload?.bulkUpload ?? true
sanitized.upload.staticDir = sanitized.upload.staticDir || sanitized.slug
@@ -195,9 +200,6 @@ export const sanitizeCollection = async (
}
if (sanitized.auth) {
- // sanitize fields for reserved names
- sanitizeAuthFields(sanitized.fields, sanitized)
-
sanitized.auth = addDefaultsToAuthConfig(
typeof sanitized.auth === 'boolean' ? {} : sanitized.auth,
)
diff --git a/packages/payload/src/collections/config/useAsTitle.ts b/packages/payload/src/collections/config/useAsTitle.ts
index c4633f94f7..9dfb804d25 100644
--- a/packages/payload/src/collections/config/useAsTitle.ts
+++ b/packages/payload/src/collections/config/useAsTitle.ts
@@ -1,7 +1,7 @@
import type { CollectionConfig } from '../../index.js'
import { InvalidConfiguration } from '../../errors/InvalidConfiguration.js'
-import { fieldAffectsData, fieldIsVirtual } from '../../fields/config/types.js'
+import { fieldAffectsData } from '../../fields/config/types.js'
import flattenFields from '../../utilities/flattenTopLevelFields.js'
/**
diff --git a/packages/payload/src/collections/operations/create.ts b/packages/payload/src/collections/operations/create.ts
index 9f3e7f5028..95b0e0ac6c 100644
--- a/packages/payload/src/collections/operations/create.ts
+++ b/packages/payload/src/collections/operations/create.ts
@@ -9,9 +9,6 @@ import type {
TransformCollectionWithSelect,
} from '../../types/index.js'
import type {
- AfterChangeHook,
- BeforeOperationHook,
- BeforeValidateHook,
Collection,
DataFromCollectionSlug,
RequiredDataFromCollectionSlug,
@@ -225,6 +222,7 @@ export const createOperation = async <
docWithLocales: duplicatedFromDocWithLocales,
global: null,
operation: 'create',
+ overrideAccess,
req,
skipValidation:
shouldSaveDraft &&
diff --git a/packages/payload/src/collections/operations/delete.ts b/packages/payload/src/collections/operations/delete.ts
index 68a9fecbc3..15d60afbd6 100644
--- a/packages/payload/src/collections/operations/delete.ts
+++ b/packages/payload/src/collections/operations/delete.ts
@@ -5,7 +5,6 @@ import type { AccessResult } from '../../config/types.js'
import type { CollectionSlug } from '../../index.js'
import type { PayloadRequest, PopulateType, SelectType, Where } from '../../types/index.js'
import type {
- BeforeOperationHook,
BulkOperationResult,
Collection,
DataFromCollectionSlug,
diff --git a/packages/payload/src/collections/operations/deleteByID.ts b/packages/payload/src/collections/operations/deleteByID.ts
index add2bd8445..be3cfe5416 100644
--- a/packages/payload/src/collections/operations/deleteByID.ts
+++ b/packages/payload/src/collections/operations/deleteByID.ts
@@ -6,7 +6,7 @@ import type {
SelectType,
TransformCollectionWithSelect,
} from '../../types/index.js'
-import type { BeforeOperationHook, Collection, DataFromCollectionSlug } from '../config/types.js'
+import type { Collection, DataFromCollectionSlug } from '../config/types.js'
import executeAccess from '../../auth/executeAccess.js'
import { hasWhereAccessResult } from '../../auth/types.js'
diff --git a/packages/payload/src/collections/operations/local/create.ts b/packages/payload/src/collections/operations/local/create.ts
index 5e66f07638..a9c558dcc4 100644
--- a/packages/payload/src/collections/operations/local/create.ts
+++ b/packages/payload/src/collections/operations/local/create.ts
@@ -139,6 +139,7 @@ export default async function createLocal<
select,
showHiddenFields,
} = options
+
const collection = payload.collections[collectionSlug]
if (!collection) {
@@ -148,6 +149,7 @@ export default async function createLocal<
}
const req = await createLocalReq(options, payload)
+
req.file = file ?? (await getFileByPath(filePath))
return createOperation({
diff --git a/packages/payload/src/collections/operations/local/duplicate.ts b/packages/payload/src/collections/operations/local/duplicate.ts
index 7ae8637244..82a9459d3f 100644
--- a/packages/payload/src/collections/operations/local/duplicate.ts
+++ b/packages/payload/src/collections/operations/local/duplicate.ts
@@ -109,6 +109,7 @@ export async function duplicate<
select,
showHiddenFields,
} = options
+
const collection = payload.collections[collectionSlug]
if (!collection) {
diff --git a/packages/payload/src/collections/operations/utilities/update.ts b/packages/payload/src/collections/operations/utilities/update.ts
index acf6880868..b77948e6cb 100644
--- a/packages/payload/src/collections/operations/utilities/update.ts
+++ b/packages/payload/src/collections/operations/utilities/update.ts
@@ -234,6 +234,7 @@ export const updateDocument = async <
docWithLocales: undefined,
global: null,
operation: 'update',
+ overrideAccess,
req,
skipValidation:
shouldSaveDraft &&
diff --git a/packages/payload/src/config/sanitize.ts b/packages/payload/src/config/sanitize.ts
index d420c5826a..3a51695db7 100644
--- a/packages/payload/src/config/sanitize.ts
+++ b/packages/payload/src/config/sanitize.ts
@@ -59,6 +59,7 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial
// add default user collection if none provided
if (!sanitizedConfig?.admin?.user) {
const firstCollectionWithAuth = sanitizedConfig.collections.find(({ auth }) => Boolean(auth))
+
if (firstCollectionWithAuth) {
sanitizedConfig.admin.user = firstCollectionWithAuth.slug
} else {
@@ -70,6 +71,7 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial
const userCollection = sanitizedConfig.collections.find(
({ slug }) => slug === sanitizedConfig.admin.user,
)
+
if (!userCollection || !userCollection.auth) {
throw new InvalidConfiguration(
`${sanitizedConfig.admin.user} is not a valid admin user collection`,
diff --git a/packages/payload/src/exports/i18n/lv.ts b/packages/payload/src/exports/i18n/lv.ts
new file mode 100644
index 0000000000..b94af42339
--- /dev/null
+++ b/packages/payload/src/exports/i18n/lv.ts
@@ -0,0 +1 @@
+export { lv } from '@payloadcms/translations/languages/lv'
diff --git a/packages/payload/src/collections/config/reservedFieldNames.spec.ts b/packages/payload/src/fields/config/reservedFieldNames.spec.ts
similarity index 97%
rename from packages/payload/src/collections/config/reservedFieldNames.spec.ts
rename to packages/payload/src/fields/config/reservedFieldNames.spec.ts
index 5254b9d256..cfaaeb1f31 100644
--- a/packages/payload/src/collections/config/reservedFieldNames.spec.ts
+++ b/packages/payload/src/fields/config/reservedFieldNames.spec.ts
@@ -2,7 +2,7 @@ import type { Config } from '../../config/types.js'
import type { CollectionConfig, Field } from '../../index.js'
import { ReservedFieldName } from '../../errors/index.js'
-import { sanitizeCollection } from './sanitize.js'
+import { sanitizeCollection } from '../../collections/config/sanitize.js'
describe('reservedFieldNames - collections -', () => {
const config = {
@@ -25,6 +25,7 @@ describe('reservedFieldNames - collections -', () => {
label: 'some-collection',
},
]
+
await expect(async () => {
await sanitizeCollection(
// @ts-expect-error
@@ -53,6 +54,7 @@ describe('reservedFieldNames - collections -', () => {
label: 'some-collection',
},
]
+
await expect(async () => {
await sanitizeCollection(
// @ts-expect-error
@@ -93,6 +95,7 @@ describe('reservedFieldNames - collections -', () => {
label: 'some-collection',
},
]
+
await expect(async () => {
await sanitizeCollection(
// @ts-expect-error
@@ -121,6 +124,7 @@ describe('reservedFieldNames - collections -', () => {
label: 'some-collection',
},
]
+
await expect(async () => {
await sanitizeCollection(
// @ts-expect-error
@@ -149,6 +153,7 @@ describe('reservedFieldNames - collections -', () => {
label: 'some-collection',
},
]
+
await expect(async () => {
await sanitizeCollection(
// @ts-expect-error
diff --git a/packages/payload/src/fields/config/reservedFieldNames.ts b/packages/payload/src/fields/config/reservedFieldNames.ts
new file mode 100644
index 0000000000..394ac10bba
--- /dev/null
+++ b/packages/payload/src/fields/config/reservedFieldNames.ts
@@ -0,0 +1,48 @@
+/**
+ * Reserved field names for collections with auth config enabled
+ */
+export const reservedBaseAuthFieldNames = [
+ /* 'email',
+ 'resetPasswordToken',
+ 'resetPasswordExpiration', */
+ 'salt',
+ 'hash',
+]
+
+/**
+ * Reserved field names for auth collections with verify: true
+ */
+export const reservedVerifyFieldNames = [
+ /* '_verified', '_verificationToken' */
+]
+
+/**
+ * Reserved field names for auth collections with useApiKey: true
+ */
+export const reservedAPIKeyFieldNames = [
+ /* 'enableAPIKey', 'apiKeyIndex', 'apiKey' */
+]
+
+/**
+ * Reserved field names for collections with upload config enabled
+ */
+export const reservedBaseUploadFieldNames = [
+ 'file',
+ /* 'mimeType',
+ 'thumbnailURL',
+ 'width',
+ 'height',
+ 'filesize',
+ 'filename',
+ 'url',
+ 'focalX',
+ 'focalY',
+ 'sizes', */
+]
+
+/**
+ * Reserved field names for collections with versions enabled
+ */
+export const reservedVersionsFieldNames = [
+ /* '__v', '_status' */
+]
diff --git a/packages/payload/src/fields/config/sanitize.spec.ts b/packages/payload/src/fields/config/sanitize.spec.ts
index 3da7508577..3ebad93b3b 100644
--- a/packages/payload/src/fields/config/sanitize.spec.ts
+++ b/packages/payload/src/fields/config/sanitize.spec.ts
@@ -11,9 +11,12 @@ import type {
import { InvalidFieldName, InvalidFieldRelationship, MissingFieldType } from '../../errors/index.js'
import { sanitizeFields } from './sanitize.js'
+import { CollectionConfig } from '../../index.js'
describe('sanitizeFields', () => {
const config = {} as Config
+ const collectionConfig = {} as CollectionConfig
+
it('should throw on missing type field', async () => {
const fields: Field[] = [
// @ts-expect-error
@@ -22,14 +25,17 @@ describe('sanitizeFields', () => {
label: 'some-collection',
},
]
+
await expect(async () => {
await sanitizeFields({
config,
+ collectionConfig,
fields,
validRelationships: [],
})
}).rejects.toThrow(MissingFieldType)
})
+
it('should throw on invalid field name', async () => {
const fields: Field[] = [
{
@@ -38,9 +44,11 @@ describe('sanitizeFields', () => {
label: 'some.collection',
},
]
+
await expect(async () => {
await sanitizeFields({
config,
+ collectionConfig,
fields,
validRelationships: [],
})
@@ -55,17 +63,21 @@ describe('sanitizeFields', () => {
type: 'text',
},
]
+
const sanitizedField = (
await sanitizeFields({
config,
+ collectionConfig,
fields,
validRelationships: [],
})
)[0] as TextField
+
expect(sanitizedField.name).toStrictEqual('someField')
expect(sanitizedField.label).toStrictEqual('Some Field')
expect(sanitizedField.type).toStrictEqual('text')
})
+
it('should allow auto-label override', async () => {
const fields: Field[] = [
{
@@ -74,13 +86,16 @@ describe('sanitizeFields', () => {
label: 'Do not label',
},
]
+
const sanitizedField = (
await sanitizeFields({
config,
+ collectionConfig,
fields,
validRelationships: [],
})
)[0] as TextField
+
expect(sanitizedField.name).toStrictEqual('someField')
expect(sanitizedField.label).toStrictEqual('Do not label')
expect(sanitizedField.type).toStrictEqual('text')
@@ -95,13 +110,16 @@ describe('sanitizeFields', () => {
label: false,
},
]
+
const sanitizedField = (
await sanitizeFields({
config,
+ collectionConfig,
fields,
validRelationships: [],
})
)[0] as TextField
+
expect(sanitizedField.name).toStrictEqual('someField')
expect(sanitizedField.label).toStrictEqual(false)
expect(sanitizedField.type).toStrictEqual('text')
@@ -119,18 +137,22 @@ describe('sanitizeFields', () => {
],
label: false,
}
+
const sanitizedField = (
await sanitizeFields({
config,
+ collectionConfig,
fields: [arrayField],
validRelationships: [],
})
)[0] as ArrayField
+
expect(sanitizedField.name).toStrictEqual('items')
expect(sanitizedField.label).toStrictEqual(false)
expect(sanitizedField.type).toStrictEqual('array')
expect(sanitizedField.labels).toBeUndefined()
})
+
it('should allow label opt-out for blocks', async () => {
const fields: Field[] = [
{
@@ -150,13 +172,16 @@ describe('sanitizeFields', () => {
label: false,
},
]
+
const sanitizedField = (
await sanitizeFields({
config,
+ collectionConfig,
fields,
validRelationships: [],
})
)[0] as BlocksField
+
expect(sanitizedField.name).toStrictEqual('noLabelBlock')
expect(sanitizedField.label).toStrictEqual(false)
expect(sanitizedField.type).toStrictEqual('blocks')
@@ -177,13 +202,16 @@ describe('sanitizeFields', () => {
],
},
]
+
const sanitizedField = (
await sanitizeFields({
config,
+ collectionConfig,
fields,
validRelationships: [],
})
)[0] as ArrayField
+
expect(sanitizedField.name).toStrictEqual('items')
expect(sanitizedField.label).toStrictEqual('Items')
expect(sanitizedField.type).toStrictEqual('array')
@@ -203,13 +231,16 @@ describe('sanitizeFields', () => {
],
},
]
+
const sanitizedField = (
await sanitizeFields({
config,
+ collectionConfig,
fields,
validRelationships: [],
})
)[0] as BlocksField
+
expect(sanitizedField.name).toStrictEqual('specialBlock')
expect(sanitizedField.label).toStrictEqual('Special Block')
expect(sanitizedField.type).toStrictEqual('blocks')
@@ -217,6 +248,7 @@ describe('sanitizeFields', () => {
plural: 'Special Blocks',
singular: 'Special Block',
})
+
expect((sanitizedField.blocks[0].fields[0] as NumberField).label).toStrictEqual('Test Number')
})
})
@@ -232,8 +264,9 @@ describe('sanitizeFields', () => {
relationTo: 'some-collection',
},
]
+
await expect(async () => {
- await sanitizeFields({ config, fields, validRelationships })
+ await sanitizeFields({ config, collectionConfig, fields, validRelationships })
}).not.toThrow()
})
@@ -247,8 +280,9 @@ describe('sanitizeFields', () => {
relationTo: ['some-collection', 'another-collection'],
},
]
+
await expect(async () => {
- await sanitizeFields({ config, fields, validRelationships })
+ await sanitizeFields({ config, collectionConfig, fields, validRelationships })
}).not.toThrow()
})
@@ -265,6 +299,7 @@ describe('sanitizeFields', () => {
},
],
}
+
const fields: Field[] = [
{
name: 'layout',
@@ -273,8 +308,9 @@ describe('sanitizeFields', () => {
label: 'Layout Blocks',
},
]
+
await expect(async () => {
- await sanitizeFields({ config, fields, validRelationships })
+ await sanitizeFields({ config, collectionConfig, fields, validRelationships })
}).not.toThrow()
})
@@ -288,8 +324,9 @@ describe('sanitizeFields', () => {
relationTo: 'not-valid',
},
]
+
await expect(async () => {
- await sanitizeFields({ config, fields, validRelationships })
+ await sanitizeFields({ config, collectionConfig, fields, validRelationships })
}).rejects.toThrow(InvalidFieldRelationship)
})
@@ -303,8 +340,9 @@ describe('sanitizeFields', () => {
relationTo: ['some-collection', 'not-valid'],
},
]
+
await expect(async () => {
- await sanitizeFields({ config, fields, validRelationships })
+ await sanitizeFields({ config, collectionConfig, fields, validRelationships })
}).rejects.toThrow(InvalidFieldRelationship)
})
@@ -321,6 +359,7 @@ describe('sanitizeFields', () => {
},
],
}
+
const fields: Field[] = [
{
name: 'layout',
@@ -329,8 +368,9 @@ describe('sanitizeFields', () => {
label: 'Layout Blocks',
},
]
+
await expect(async () => {
- await sanitizeFields({ config, fields, validRelationships })
+ await sanitizeFields({ config, collectionConfig, fields, validRelationships })
}).rejects.toThrow(InvalidFieldRelationship)
})
@@ -346,19 +386,23 @@ describe('sanitizeFields', () => {
const sanitizedField = (
await sanitizeFields({
config,
+ collectionConfig,
fields,
validRelationships: [],
})
)[0] as CheckboxField
+
expect(sanitizedField.defaultValue).toStrictEqual(false)
})
it('should return empty field array if no fields', async () => {
const sanitizedFields = await sanitizeFields({
config,
+ collectionConfig,
fields: [],
validRelationships: [],
})
+
expect(sanitizedFields).toStrictEqual([])
})
})
@@ -385,9 +429,11 @@ describe('sanitizeFields', () => {
label: false,
},
]
+
const sanitizedField = (
await sanitizeFields({
config,
+ collectionConfig,
fields,
validRelationships: [],
})
@@ -416,9 +462,11 @@ describe('sanitizeFields', () => {
label: false,
},
]
+
const sanitizedField = (
await sanitizeFields({
config,
+ collectionConfig,
fields,
validRelationships: [],
})
diff --git a/packages/payload/src/fields/config/sanitize.ts b/packages/payload/src/fields/config/sanitize.ts
index 9495f3773c..34deb9718e 100644
--- a/packages/payload/src/fields/config/sanitize.ts
+++ b/packages/payload/src/fields/config/sanitize.ts
@@ -17,6 +17,7 @@ import {
MissingEditorProp,
MissingFieldType,
} from '../../errors/index.js'
+import { ReservedFieldName } from '../../errors/ReservedFieldName.js'
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
import { baseBlockFields } from '../baseFields/baseBlockFields.js'
import { baseIDField } from '../baseFields/baseIDField.js'
@@ -24,14 +25,24 @@ import { baseTimezoneField } from '../baseFields/timezone/baseField.js'
import { defaultTimezones } from '../baseFields/timezone/defaultTimezones.js'
import { setDefaultBeforeDuplicate } from '../setDefaultBeforeDuplicate.js'
import { validations } from '../validations.js'
+import {
+ reservedAPIKeyFieldNames,
+ reservedBaseAuthFieldNames,
+ reservedBaseUploadFieldNames,
+ reservedVerifyFieldNames,
+} from './reservedFieldNames.js'
import { sanitizeJoinField } from './sanitizeJoinField.js'
-import { fieldAffectsData, fieldIsLocalized, tabHasName } from './types.js'
+import { fieldAffectsData as _fieldAffectsData, fieldIsLocalized, tabHasName } from './types.js'
type Args = {
collectionConfig?: CollectionConfig
config: Config
existingFieldNames?: Set
fields: Field[]
+ /**
+ * Used to prevent unnecessary sanitization of fields that are not top-level.
+ */
+ isTopLevelField?: boolean
joinPath?: string
/**
* When not passed in, assume that join are not supported (globals, arrays, blocks)
@@ -39,7 +50,6 @@ type Args = {
joins?: SanitizedJoins
parentIsLocalized: boolean
polymorphicJoins?: SanitizedJoin[]
-
/**
* If true, a richText field will require an editor property to be set, as the sanitizeFields function will not add it from the payload config if not present.
*
@@ -59,9 +69,11 @@ type Args = {
}
export const sanitizeFields = async ({
+ collectionConfig,
config,
existingFieldNames = new Set(),
fields,
+ isTopLevelField = true,
joinPath = '',
joins,
parentIsLocalized,
@@ -80,6 +92,7 @@ export const sanitizeFields = async ({
if ('_sanitized' in field && field._sanitized === true) {
continue
}
+
if ('_sanitized' in field) {
field._sanitized = true
}
@@ -88,8 +101,39 @@ export const sanitizeFields = async ({
throw new MissingFieldType(field)
}
+ const fieldAffectsData = _fieldAffectsData(field)
+
+ if (isTopLevelField && fieldAffectsData && field.name) {
+ if (collectionConfig && collectionConfig.upload) {
+ if (reservedBaseUploadFieldNames.includes(field.name)) {
+ throw new ReservedFieldName(field, field.name)
+ }
+ }
+
+ if (
+ collectionConfig &&
+ collectionConfig.auth &&
+ typeof collectionConfig.auth === 'object' &&
+ !collectionConfig.auth.disableLocalStrategy
+ ) {
+ if (reservedBaseAuthFieldNames.includes(field.name)) {
+ throw new ReservedFieldName(field, field.name)
+ }
+
+ if (collectionConfig.auth.verify) {
+ if (reservedAPIKeyFieldNames.includes(field.name)) {
+ throw new ReservedFieldName(field, field.name)
+ }
+
+ if (reservedVerifyFieldNames.includes(field.name)) {
+ throw new ReservedFieldName(field, field.name)
+ }
+ }
+ }
+ }
+
// assert that field names do not contain forbidden characters
- if (fieldAffectsData(field) && field.name.includes('.')) {
+ if (fieldAffectsData && field.name.includes('.')) {
throw new InvalidFieldName(field, field.name)
}
@@ -122,6 +166,7 @@ export const sanitizeFields = async ({
const relationships = Array.isArray(field.relationTo)
? field.relationTo
: [field.relationTo]
+
relationships.forEach((relationship: string) => {
if (!validRelationships.includes(relationship)) {
throw new InvalidFieldRelationship(field, relationship)
@@ -135,6 +180,7 @@ export const sanitizeFields = async ({
)
field.minRows = field.min
}
+
if (field.max && !field.maxRows) {
console.warn(
`(payload): The "max" property is deprecated for the Relationship field "${field.name}" and will be removed in a future version. Please use "maxRows" instead.`,
@@ -160,7 +206,7 @@ export const sanitizeFields = async ({
field.labels = field.labels || formatLabels(field.name)
}
- if (fieldAffectsData(field)) {
+ if (fieldAffectsData) {
if (existingFieldNames.has(field.name)) {
throw new DuplicateFieldName(field.name)
} else if (!['blockName', 'id'].includes(field.name)) {
@@ -254,9 +300,11 @@ export const sanitizeFields = async ({
block.fields = block.fields.concat(baseBlockFields)
block.labels = !block.labels ? formatLabels(block.slug) : block.labels
block.fields = await sanitizeFields({
+ collectionConfig,
config,
existingFieldNames: new Set(),
fields: block.fields,
+ isTopLevelField: false,
parentIsLocalized: parentIsLocalized || field.localized,
requireFieldLevelRichTextEditor,
richTextSanitizationPromises,
@@ -267,12 +315,12 @@ export const sanitizeFields = async ({
if ('fields' in field && field.fields) {
field.fields = await sanitizeFields({
+ collectionConfig,
config,
- existingFieldNames: fieldAffectsData(field) ? new Set() : existingFieldNames,
+ existingFieldNames: fieldAffectsData ? new Set() : existingFieldNames,
fields: field.fields,
- joinPath: fieldAffectsData(field)
- ? `${joinPath ? joinPath + '.' : ''}${field.name}`
- : joinPath,
+ isTopLevelField: isTopLevelField && !fieldAffectsData,
+ joinPath: fieldAffectsData ? `${joinPath ? joinPath + '.' : ''}${field.name}` : joinPath,
joins,
parentIsLocalized: parentIsLocalized || fieldIsLocalized(field),
polymorphicJoins,
@@ -285,7 +333,10 @@ export const sanitizeFields = async ({
if (field.type === 'tabs') {
for (let j = 0; j < field.tabs.length; j++) {
const tab = field.tabs[j]
- if (tabHasName(tab) && typeof tab.label === 'undefined') {
+
+ const isNamedTab = tabHasName(tab)
+
+ if (isNamedTab && typeof tab.label === 'undefined') {
tab.label = toWords(tab.name)
}
@@ -296,21 +347,24 @@ export const sanitizeFields = async ({
!tab.id
) {
// Always attach a UUID to tabs with a condition so there's no conflicts even if there are duplicate nested names
- tab.id = tabHasName(tab) ? `${tab.name}_${uuid()}` : uuid()
+ tab.id = isNamedTab ? `${tab.name}_${uuid()}` : uuid()
}
tab.fields = await sanitizeFields({
+ collectionConfig,
config,
- existingFieldNames: tabHasName(tab) ? new Set() : existingFieldNames,
+ existingFieldNames: isNamedTab ? new Set() : existingFieldNames,
fields: tab.fields,
- joinPath: tabHasName(tab) ? `${joinPath ? joinPath + '.' : ''}${tab.name}` : joinPath,
+ isTopLevelField: isTopLevelField && !isNamedTab,
+ joinPath: isNamedTab ? `${joinPath ? joinPath + '.' : ''}${tab.name}` : joinPath,
joins,
- parentIsLocalized: parentIsLocalized || (tabHasName(tab) && tab.localized),
+ parentIsLocalized: parentIsLocalized || (isNamedTab && tab.localized),
polymorphicJoins,
requireFieldLevelRichTextEditor,
richTextSanitizationPromises,
validRelationships,
})
+
field.tabs[j] = tab
}
}
diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts
index a8aec98c94..55642663fc 100644
--- a/packages/payload/src/fields/config/types.ts
+++ b/packages/payload/src/fields/config/types.ts
@@ -404,7 +404,6 @@ export type LabelsClient = {
}
export type BaseValidateOptions = {
- /**
/**
* The data of the nearest parent block. If the field is not within a block, `blockData` will be equal to `undefined`.
*/
@@ -414,6 +413,10 @@ export type BaseValidateOptions = {
event?: 'onChange' | 'submit'
id?: number | string
operation?: Operation
+ /**
+ * The `overrideAccess` flag that was attached to the request. This is used to bypass access control checks for fields.
+ */
+ overrideAccess?: boolean
/**
* The path of the field, e.g. ["group", "myArray", 1, "textField"]. The path is the schemaPath but with indexes and would be used in the context of field data, not field schemas.
*/
@@ -716,7 +719,7 @@ export type DateFieldClient = {
} & FieldBaseClient &
Pick
-export type GroupField = {
+export type GroupBase = {
admin?: {
components?: {
afterInput?: CustomComponent[]
@@ -726,6 +729,11 @@ export type GroupField = {
hideGutter?: boolean
} & Admin
fields: Field[]
+ type: 'group'
+ validate?: Validate
+} & Omit
+
+export type NamedGroupField = {
/** Customize generated GraphQL and Typescript schema names.
* By default, it is bound to the collection.
*
@@ -733,15 +741,39 @@ export type GroupField = {
* **Note**: Top level types can collide, ensure they are unique amongst collections, arrays, groups, blocks, tabs.
*/
interfaceName?: string
- type: 'group'
- validate?: Validate
-} & Omit
+} & GroupBase
-export type GroupFieldClient = {
- admin?: AdminClient & Pick
+export type UnnamedGroupField = {
+ interfaceName?: never
+ /**
+ * Can be either:
+ * - A string, which will be used as the tab's label.
+ * - An object, where the key is the language code and the value is the label.
+ */
+ label:
+ | {
+ [selectedLanguage: string]: string
+ }
+ | LabelFunction
+ | string
+ localized?: never
+} & Omit
+
+export type GroupField = NamedGroupField | UnnamedGroupField
+
+export type NamedGroupFieldClient = {
+ admin?: AdminClient & Pick
fields: ClientField[]
} & Omit &
- Pick
+ Pick
+
+export type UnnamedGroupFieldClient = {
+ admin?: AdminClient & Pick
+ fields: ClientField[]
+} & Omit &
+ Pick
+
+export type GroupFieldClient = NamedGroupFieldClient | UnnamedGroupFieldClient
export type RowField = {
admin?: Omit
@@ -1176,7 +1208,7 @@ export type PolymorphicRelationshipField = {
export type PolymorphicRelationshipFieldClient = {
admin?: {
- sortOptions?: Pick
+ sortOptions?: PolymorphicRelationshipField['admin']['sortOptions']
} & RelationshipAdminClient
} & Pick &
SharedRelationshipPropertiesClient
@@ -1608,6 +1640,7 @@ export type FlattenedBlocksField = {
export type FlattenedGroupField = {
flattenedFields: FlattenedField[]
+ name: string
} & GroupField
export type FlattenedArrayField = {
@@ -1725,9 +1758,9 @@ export type FieldAffectingData =
| CodeField
| DateField
| EmailField
- | GroupField
| JoinField
| JSONField
+ | NamedGroupField
| NumberField
| PointField
| RadioField
@@ -1746,9 +1779,9 @@ export type FieldAffectingDataClient =
| CodeFieldClient
| DateFieldClient
| EmailFieldClient
- | GroupFieldClient
| JoinFieldClient
| JSONFieldClient
+ | NamedGroupFieldClient
| NumberFieldClient
| PointFieldClient
| RadioFieldClient
@@ -1768,8 +1801,8 @@ export type NonPresentationalField =
| CollapsibleField
| DateField
| EmailField
- | GroupField
| JSONField
+ | NamedGroupField
| NumberField
| PointField
| RadioField
@@ -1790,8 +1823,8 @@ export type NonPresentationalFieldClient =
| CollapsibleFieldClient
| DateFieldClient
| EmailFieldClient
- | GroupFieldClient
| JSONFieldClient
+ | NamedGroupFieldClient
| NumberFieldClient
| PointFieldClient
| RadioFieldClient
diff --git a/packages/payload/src/fields/hooks/afterChange/promise.ts b/packages/payload/src/fields/hooks/afterChange/promise.ts
index dc18f307d0..8d348fe209 100644
--- a/packages/payload/src/fields/hooks/afterChange/promise.ts
+++ b/packages/payload/src/fields/hooks/afterChange/promise.ts
@@ -212,25 +212,47 @@ export const promise = async ({
}
case 'group': {
- await traverseFields({
- blockData,
- collection,
- context,
- data,
- doc,
- fields: field.fields,
- global,
- operation,
- parentIndexPath: '',
- parentIsLocalized: parentIsLocalized || field.localized,
- parentPath: path,
- parentSchemaPath: schemaPath,
- previousDoc,
- previousSiblingDoc: previousDoc[field.name] as JsonObject,
- req,
- siblingData: (siblingData?.[field.name] as JsonObject) || {},
- siblingDoc: siblingDoc[field.name] as JsonObject,
- })
+ if (fieldAffectsData(field)) {
+ await traverseFields({
+ blockData,
+ collection,
+ context,
+ data,
+ doc,
+ fields: field.fields,
+ global,
+ operation,
+ parentIndexPath: '',
+ parentIsLocalized: parentIsLocalized || field.localized,
+ parentPath: path,
+ parentSchemaPath: schemaPath,
+ previousDoc,
+ previousSiblingDoc: previousDoc[field.name] as JsonObject,
+ req,
+ siblingData: (siblingData?.[field.name] as JsonObject) || {},
+ siblingDoc: siblingDoc[field.name] as JsonObject,
+ })
+ } else {
+ await traverseFields({
+ blockData,
+ collection,
+ context,
+ data,
+ doc,
+ fields: field.fields,
+ global,
+ operation,
+ parentIndexPath: indexPath,
+ parentIsLocalized,
+ parentPath,
+ parentSchemaPath: schemaPath,
+ previousDoc,
+ previousSiblingDoc: { ...previousSiblingDoc },
+ req,
+ siblingData: siblingData || {},
+ siblingDoc: { ...siblingDoc },
+ })
+ }
break
}
diff --git a/packages/payload/src/fields/hooks/afterRead/promise.ts b/packages/payload/src/fields/hooks/afterRead/promise.ts
index 20612d465c..a06c7ab706 100644
--- a/packages/payload/src/fields/hooks/afterRead/promise.ts
+++ b/packages/payload/src/fields/hooks/afterRead/promise.ts
@@ -186,7 +186,7 @@ export const promise = async ({
case 'group': {
// Fill groups with empty objects so fields with hooks within groups can populate
// themselves virtually as necessary
- if (typeof siblingDoc[field.name] === 'undefined') {
+ if (fieldAffectsData(field) && typeof siblingDoc[field.name] === 'undefined') {
siblingDoc[field.name] = {}
}
@@ -609,45 +609,78 @@ export const promise = async ({
}
case 'group': {
- let groupDoc = siblingDoc[field.name] as JsonObject
+ if (fieldAffectsData(field)) {
+ let groupDoc = siblingDoc[field.name] as JsonObject
- if (typeof siblingDoc[field.name] !== 'object') {
- groupDoc = {}
+ if (typeof siblingDoc[field.name] !== 'object') {
+ groupDoc = {}
+ }
+
+ const groupSelect = select?.[field.name]
+
+ traverseFields({
+ blockData,
+ collection,
+ context,
+ currentDepth,
+ depth,
+ doc,
+ draft,
+ fallbackLocale,
+ fieldPromises,
+ fields: field.fields,
+ findMany,
+ flattenLocales,
+ global,
+ locale,
+ overrideAccess,
+ parentIndexPath: '',
+ parentIsLocalized: parentIsLocalized || field.localized,
+ parentPath: path,
+ parentSchemaPath: schemaPath,
+ populate,
+ populationPromises,
+ req,
+ select: typeof groupSelect === 'object' ? groupSelect : undefined,
+ selectMode,
+ showHiddenFields,
+ siblingDoc: groupDoc,
+ triggerAccessControl,
+ triggerHooks,
+ })
+ } else {
+ traverseFields({
+ blockData,
+ collection,
+ context,
+ currentDepth,
+ depth,
+ doc,
+ draft,
+ fallbackLocale,
+ fieldPromises,
+ fields: field.fields,
+ findMany,
+ flattenLocales,
+ global,
+ locale,
+ overrideAccess,
+ parentIndexPath: indexPath,
+ parentIsLocalized,
+ parentPath,
+ parentSchemaPath: schemaPath,
+ populate,
+ populationPromises,
+ req,
+ select,
+ selectMode,
+ showHiddenFields,
+ siblingDoc,
+ triggerAccessControl,
+ triggerHooks,
+ })
}
- const groupSelect = select?.[field.name]
-
- traverseFields({
- blockData,
- collection,
- context,
- currentDepth,
- depth,
- doc,
- draft,
- fallbackLocale,
- fieldPromises,
- fields: field.fields,
- findMany,
- flattenLocales,
- global,
- locale,
- overrideAccess,
- parentIndexPath: '',
- parentIsLocalized: parentIsLocalized || field.localized,
- parentPath: path,
- parentSchemaPath: schemaPath,
- populate,
- populationPromises,
- req,
- select: typeof groupSelect === 'object' ? groupSelect : undefined,
- selectMode,
- showHiddenFields,
- siblingDoc: groupDoc,
- triggerAccessControl,
- triggerHooks,
- })
-
break
}
diff --git a/packages/payload/src/fields/hooks/beforeChange/index.ts b/packages/payload/src/fields/hooks/beforeChange/index.ts
index 3df18dbc56..9b7604f841 100644
--- a/packages/payload/src/fields/hooks/beforeChange/index.ts
+++ b/packages/payload/src/fields/hooks/beforeChange/index.ts
@@ -8,6 +8,7 @@ import type { JsonObject, Operation, PayloadRequest } from '../../../types/index
import { ValidationError } from '../../../errors/index.js'
import { deepCopyObjectSimple } from '../../../utilities/deepCopyObject.js'
import { traverseFields } from './traverseFields.js'
+
export type Args = {
collection: null | SanitizedCollectionConfig
context: RequestContext
@@ -17,6 +18,7 @@ export type Args = {
global: null | SanitizedGlobalConfig
id?: number | string
operation: Operation
+ overrideAccess?: boolean
req: PayloadRequest
skipValidation?: boolean
}
@@ -39,6 +41,7 @@ export const beforeChange = async ({
docWithLocales,
global,
operation,
+ overrideAccess,
req,
skipValidation,
}: Args): Promise => {
@@ -59,6 +62,7 @@ export const beforeChange = async ({
global,
mergeLocaleActions,
operation,
+ overrideAccess,
parentIndexPath: '',
parentIsLocalized: false,
parentPath: '',
diff --git a/packages/payload/src/fields/hooks/beforeChange/promise.ts b/packages/payload/src/fields/hooks/beforeChange/promise.ts
index 87a323a852..27b5978c48 100644
--- a/packages/payload/src/fields/hooks/beforeChange/promise.ts
+++ b/packages/payload/src/fields/hooks/beforeChange/promise.ts
@@ -45,6 +45,7 @@ type Args = {
id?: number | string
mergeLocaleActions: (() => Promise | void)[]
operation: Operation
+ overrideAccess: boolean
parentIndexPath: string
parentIsLocalized: boolean
parentPath: string
@@ -80,6 +81,7 @@ export const promise = async ({
global,
mergeLocaleActions,
operation,
+ overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
@@ -176,6 +178,7 @@ export const promise = async ({
object,
object
>
+
const validationResult = await validateFn(valueToValidate as never, {
...field,
id,
@@ -186,6 +189,7 @@ export const promise = async ({
// @ts-expect-error
jsonError,
operation,
+ overrideAccess,
path: pathSegments,
preferences: { fields: {} },
previousValue: siblingDoc[field.name],
@@ -261,6 +265,7 @@ export const promise = async ({
global,
mergeLocaleActions,
operation,
+ overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
@@ -326,6 +331,7 @@ export const promise = async ({
global,
mergeLocaleActions,
operation,
+ overrideAccess,
parentIndexPath: '',
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: path + '.' + rowIndex,
@@ -368,6 +374,7 @@ export const promise = async ({
global,
mergeLocaleActions,
operation,
+ overrideAccess,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath,
@@ -383,17 +390,42 @@ export const promise = async ({
}
case 'group': {
- if (typeof siblingData[field.name] !== 'object') {
- siblingData[field.name] = {}
+ let groupSiblingData = siblingData
+ let groupSiblingDoc = siblingDoc
+ let groupSiblingDocWithLocales = siblingDocWithLocales
+
+ const isNamedGroup = fieldAffectsData(field)
+
+ if (isNamedGroup) {
+ if (typeof siblingData[field.name] !== 'object') {
+ siblingData[field.name] = {}
+ }
+
+ if (typeof siblingDoc[field.name] !== 'object') {
+ siblingDoc[field.name] = {}
+ }
+
+ if (typeof siblingDocWithLocales[field.name] !== 'object') {
+ siblingDocWithLocales[field.name] = {}
+ }
+ if (typeof siblingData[field.name] !== 'object') {
+ siblingData[field.name] = {}
+ }
+
+ if (typeof siblingDoc[field.name] !== 'object') {
+ siblingDoc[field.name] = {}
+ }
+
+ if (typeof siblingDocWithLocales[field.name] !== 'object') {
+ siblingDocWithLocales[field.name] = {}
+ }
+
+ groupSiblingData = siblingData[field.name] as JsonObject
+ groupSiblingDoc = siblingDoc[field.name] as JsonObject
+ groupSiblingDocWithLocales = siblingDocWithLocales[field.name] as JsonObject
}
- if (typeof siblingDoc[field.name] !== 'object') {
- siblingDoc[field.name] = {}
- }
-
- if (typeof siblingDocWithLocales[field.name] !== 'object') {
- siblingDocWithLocales[field.name] = {}
- }
+ const fallbackLabel = field?.label || (isNamedGroup ? field.name : field?.type)
await traverseFields({
id,
@@ -407,22 +439,20 @@ export const promise = async ({
fieldLabelPath:
field?.label === false
? fieldLabelPath
- : buildFieldLabel(
- fieldLabelPath,
- getTranslatedLabel(field?.label || field?.name, req.i18n),
- ),
+ : buildFieldLabel(fieldLabelPath, getTranslatedLabel(fallbackLabel, req.i18n)),
fields: field.fields,
global,
mergeLocaleActions,
operation,
- parentIndexPath: '',
+ overrideAccess,
+ parentIndexPath: isNamedGroup ? '' : indexPath,
parentIsLocalized: parentIsLocalized || field.localized,
- parentPath: path,
+ parentPath: isNamedGroup ? path : parentPath,
parentSchemaPath: schemaPath,
req,
- siblingData: siblingData[field.name] as JsonObject,
- siblingDoc: siblingDoc[field.name] as JsonObject,
- siblingDocWithLocales: siblingDocWithLocales[field.name] as JsonObject,
+ siblingData: groupSiblingData,
+ siblingDoc: groupSiblingDoc,
+ siblingDocWithLocales: groupSiblingDocWithLocales,
skipValidation: skipValidationFromHere,
})
@@ -480,6 +510,7 @@ export const promise = async ({
mergeLocaleActions,
operation,
originalDoc: doc,
+ overrideAccess,
parentIsLocalized,
path: pathSegments,
previousSiblingDoc: siblingDoc,
@@ -546,6 +577,7 @@ export const promise = async ({
global,
mergeLocaleActions,
operation,
+ overrideAccess,
parentIndexPath: isNamedTab ? '' : indexPath,
parentIsLocalized: parentIsLocalized || field.localized,
parentPath: isNamedTab ? path : parentPath,
@@ -578,6 +610,7 @@ export const promise = async ({
global,
mergeLocaleActions,
operation,
+ overrideAccess,
parentIndexPath: indexPath,
parentIsLocalized,
parentPath: path,
diff --git a/packages/payload/src/fields/hooks/beforeChange/traverseFields.ts b/packages/payload/src/fields/hooks/beforeChange/traverseFields.ts
index e5f3ac316c..e784355802 100644
--- a/packages/payload/src/fields/hooks/beforeChange/traverseFields.ts
+++ b/packages/payload/src/fields/hooks/beforeChange/traverseFields.ts
@@ -36,6 +36,7 @@ type Args = {
id?: number | string
mergeLocaleActions: (() => Promise | void)[]
operation: Operation
+ overrideAccess: boolean
parentIndexPath: string
/**
* @todo make required in v4.0
@@ -78,6 +79,7 @@ export const traverseFields = async ({
global,
mergeLocaleActions,
operation,
+ overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
@@ -107,6 +109,7 @@ export const traverseFields = async ({
global,
mergeLocaleActions,
operation,
+ overrideAccess,
parentIndexPath,
parentIsLocalized,
parentPath,
diff --git a/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts b/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts
index 2daa2a91b4..81e3e4481d 100644
--- a/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts
+++ b/packages/payload/src/fields/hooks/beforeDuplicate/promise.ts
@@ -375,9 +375,10 @@ export const promise = async ({
}
}
} else {
- // Finally, we traverse fields which do not affect data here
+ // Finally, we traverse fields which do not affect data here - collapsibles, rows, unnamed groups
switch (field.type) {
case 'collapsible':
+ case 'group':
case 'row': {
await traverseFields({
id,
diff --git a/packages/payload/src/fields/hooks/beforeValidate/promise.ts b/packages/payload/src/fields/hooks/beforeValidate/promise.ts
index 2cda4e9b38..f07dbbe759 100644
--- a/packages/payload/src/fields/hooks/beforeValidate/promise.ts
+++ b/packages/payload/src/fields/hooks/beforeValidate/promise.ts
@@ -447,16 +447,23 @@ export const promise = async ({
}
case 'group': {
- if (typeof siblingData[field.name] !== 'object') {
- siblingData[field.name] = {}
- }
+ let groupSiblingData = siblingData
+ let groupSiblingDoc = siblingDoc
- if (typeof siblingDoc[field.name] !== 'object') {
- siblingDoc[field.name] = {}
- }
+ const isNamedGroup = fieldAffectsData(field)
- const groupData = siblingData[field.name] as Record
- const groupDoc = siblingDoc[field.name] as Record
+ if (isNamedGroup) {
+ if (typeof siblingData[field.name] !== 'object') {
+ siblingData[field.name] = {}
+ }
+
+ if (typeof siblingDoc[field.name] !== 'object') {
+ siblingDoc[field.name] = {}
+ }
+
+ groupSiblingData = siblingData[field.name] as Record
+ groupSiblingDoc = siblingDoc[field.name] as Record
+ }
await traverseFields({
id,
@@ -469,13 +476,13 @@ export const promise = async ({
global,
operation,
overrideAccess,
- parentIndexPath: '',
+ parentIndexPath: isNamedGroup ? '' : indexPath,
parentIsLocalized: parentIsLocalized || field.localized,
- parentPath: path,
+ parentPath: isNamedGroup ? path : parentPath,
parentSchemaPath: schemaPath,
req,
- siblingData: groupData as JsonObject,
- siblingDoc: groupDoc as JsonObject,
+ siblingData: groupSiblingData,
+ siblingDoc: groupSiblingDoc,
})
break
diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts
index ff4cfaea07..b25837a2dc 100644
--- a/packages/payload/src/index.ts
+++ b/packages/payload/src/index.ts
@@ -89,6 +89,10 @@ import { traverseFields } from './utilities/traverseFields.js'
export { default as executeAccess } from './auth/executeAccess.js'
export { executeAuthStrategies } from './auth/executeAuthStrategies.js'
+export { extractAccessFromPermission } from './auth/extractAccessFromPermission.js'
+export { getAccessResults } from './auth/getAccessResults.js'
+export { getFieldsToSign } from './auth/getFieldsToSign.js'
+export { getLoginOptions } from './auth/getLoginOptions.js'
export interface GeneratedTypes {
authUntyped: {
@@ -977,13 +981,12 @@ interface RequestContext {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface DatabaseAdapter extends BaseDatabaseAdapter {}
export type { Payload, RequestContext }
-export { extractAccessFromPermission } from './auth/extractAccessFromPermission.js'
-export { getAccessResults } from './auth/getAccessResults.js'
-export { getFieldsToSign } from './auth/getFieldsToSign.js'
export * from './auth/index.js'
+export { jwtSign } from './auth/jwt.js'
export { accessOperation } from './auth/operations/access.js'
export { forgotPasswordOperation } from './auth/operations/forgotPassword.js'
export { initOperation } from './auth/operations/init.js'
+export { checkLoginPermission } from './auth/operations/login.js'
export { loginOperation } from './auth/operations/login.js'
export { logoutOperation } from './auth/operations/logout.js'
export type { MeOperationResult } from './auth/operations/me.js'
@@ -994,6 +997,8 @@ export { resetPasswordOperation } from './auth/operations/resetPassword.js'
export { unlockOperation } from './auth/operations/unlock.js'
export { verifyEmailOperation } from './auth/operations/verifyEmail.js'
export { JWTAuthentication } from './auth/strategies/jwt.js'
+export { incrementLoginAttempts } from './auth/strategies/local/incrementLoginAttempts.js'
+export { resetLoginAttempts } from './auth/strategies/local/resetLoginAttempts.js'
export type {
AuthStrategyFunction,
AuthStrategyFunctionArgs,
@@ -1201,6 +1206,7 @@ export {
MissingFile,
NotFound,
QueryError,
+ UnverifiedEmail,
ValidationError,
ValidationErrorName,
} from './errors/index.js'
@@ -1276,6 +1282,8 @@ export type {
JSONFieldClient,
Labels,
LabelsClient,
+ NamedGroupField,
+ NamedGroupFieldClient,
NamedTab,
NonPresentationalField,
NonPresentationalFieldClient,
@@ -1312,6 +1320,8 @@ export type {
TextFieldClient,
UIField,
UIFieldClient,
+ UnnamedGroupField,
+ UnnamedGroupFieldClient,
UnnamedTab,
UploadField,
UploadFieldClient,
diff --git a/packages/payload/src/query-presets/config.ts b/packages/payload/src/query-presets/config.ts
index ba2d3ee6ff..9fd822909e 100644
--- a/packages/payload/src/query-presets/config.ts
+++ b/packages/payload/src/query-presets/config.ts
@@ -113,6 +113,16 @@ export const getQueryPresetsConfig = (config: Config): CollectionConfig => ({
: [],
required: true,
},
+ {
+ name: 'isTemp',
+ type: 'checkbox',
+ admin: {
+ description:
+ "This is a tempoary field used to determine if updating the preset would remove the user's access to it. When `true`, this record will be deleted after running the preset's `validate` function.",
+ disabled: true,
+ hidden: true,
+ },
+ },
],
hooks: {
beforeValidate: [
diff --git a/packages/payload/src/query-presets/constraints.ts b/packages/payload/src/query-presets/constraints.ts
index 33603d2f1d..921749ec12 100644
--- a/packages/payload/src/query-presets/constraints.ts
+++ b/packages/payload/src/query-presets/constraints.ts
@@ -5,6 +5,7 @@ import type { Field } from '../fields/config/types.js'
import { fieldAffectsData } from '../fields/config/types.js'
import { toWords } from '../utilities/formatLabels.js'
+import { preventLockout } from './preventLockout.js'
import { operations, type QueryPresetConstraint } from './types.js'
export const getConstraints = (config: Config): Field => ({
@@ -101,4 +102,5 @@ export const getConstraints = (config: Config): Field => ({
label: () => toWords(operation),
})),
label: 'Sharing settings',
+ validate: preventLockout,
})
diff --git a/packages/payload/src/query-presets/preventLockout.ts b/packages/payload/src/query-presets/preventLockout.ts
new file mode 100644
index 0000000000..4f285e1d08
--- /dev/null
+++ b/packages/payload/src/query-presets/preventLockout.ts
@@ -0,0 +1,92 @@
+import type { Validate } from '../fields/config/types.js'
+
+import { APIError } from '../errors/APIError.js'
+import { createLocalReq } from '../utilities/createLocalReq.js'
+import { initTransaction } from '../utilities/initTransaction.js'
+import { killTransaction } from '../utilities/killTransaction.js'
+import { queryPresetsCollectionSlug } from './config.js'
+
+/**
+ * Prevents "accidental lockouts" where a user makes an update that removes their own access to the preset.
+ * This is effectively an access control function proxied through a `validate` function.
+ * How it works:
+ * 1. Creates a temporary record with the incoming data
+ * 2. Attempts to read and update that record with the incoming user
+ * 3. If either of those fail, throws an error to the user
+ * 4. Once finished, prevents the temp record from persisting to the database
+ */
+export const preventLockout: Validate = async (
+ value,
+ { data, overrideAccess, req: incomingReq },
+) => {
+ // Use context to ensure an infinite loop doesn't occur
+ if (!incomingReq.context._preventLockout && !overrideAccess) {
+ const req = await createLocalReq(
+ {
+ context: {
+ _preventLockout: true,
+ },
+ req: {
+ user: incomingReq.user,
+ },
+ },
+ incomingReq.payload,
+ )
+
+ // Might be `null` if no transactions are enabled
+ const transaction = await initTransaction(req)
+
+ // create a temp record to validate the constraints, using the req
+ const tempPreset = await req.payload.create({
+ collection: queryPresetsCollectionSlug,
+ data: {
+ ...data,
+ isTemp: true,
+ },
+ req,
+ })
+
+ let canUpdate = false
+ let canRead = false
+
+ try {
+ await req.payload.findByID({
+ id: tempPreset.id,
+ collection: queryPresetsCollectionSlug,
+ overrideAccess: false,
+ req,
+ user: req.user,
+ })
+
+ canRead = true
+
+ await req.payload.update({
+ id: tempPreset.id,
+ collection: queryPresetsCollectionSlug,
+ data: tempPreset,
+ overrideAccess: false,
+ req,
+ user: req.user,
+ })
+
+ canUpdate = true
+ } catch (_err) {
+ if (!canRead || !canUpdate) {
+ throw new APIError('Cannot remove yourself from this preset.', 403, {}, true)
+ }
+ } finally {
+ if (transaction) {
+ await killTransaction(req)
+ } else {
+ // delete the temp record
+ await req.payload.delete({
+ id: tempPreset.id,
+ collection: queryPresetsCollectionSlug,
+ req,
+ })
+ }
+ }
+ }
+
+ return true as unknown as true
+}
diff --git a/packages/payload/src/queues/config/generateJobsJSONSchemas.ts b/packages/payload/src/queues/config/generateJobsJSONSchemas.ts
index b61ab05401..bcee9c5b6d 100644
--- a/packages/payload/src/queues/config/generateJobsJSONSchemas.ts
+++ b/packages/payload/src/queues/config/generateJobsJSONSchemas.ts
@@ -88,7 +88,7 @@ export function generateJobsJSONSchemas(
additionalProperties: false,
properties: {
...Object.fromEntries(
- jobsConfig.tasks.map((task) => {
+ (jobsConfig.tasks ?? []).map((task) => {
const normalizedTaskSlug = task.slug[0].toUpperCase() + task.slug.slice(1)
const toReturn: JSONSchema4 = {
@@ -110,7 +110,7 @@ export function generateJobsJSONSchemas(
required: ['input', 'output'],
},
},
- required: [...jobsConfig.tasks.map((task) => task.slug), 'inline'],
+ required: [...(jobsConfig.tasks ?? []).map((task) => task.slug), 'inline'],
}
}
diff --git a/packages/payload/src/queues/config/index.ts b/packages/payload/src/queues/config/index.ts
index f4cb07924d..8e04064fac 100644
--- a/packages/payload/src/queues/config/index.ts
+++ b/packages/payload/src/queues/config/index.ts
@@ -240,7 +240,6 @@ export const getDefaultJobsCollection: (config: Config) => CollectionConfig | nu
export function jobAfterRead({ config, doc }: { config: SanitizedConfig; doc: BaseJob }): BaseJob {
doc.taskStatus = getJobTaskStatus({
jobLog: doc.log || [],
- tasksConfig: config.jobs.tasks,
})
return doc
}
diff --git a/packages/payload/src/queues/config/types/index.ts b/packages/payload/src/queues/config/types/index.ts
index 916724fd8f..8082be5266 100644
--- a/packages/payload/src/queues/config/types/index.ts
+++ b/packages/payload/src/queues/config/types/index.ts
@@ -116,7 +116,7 @@ export type JobsConfig = {
/**
* Define all possible tasks here
*/
- tasks: TaskConfig[]
+ tasks?: TaskConfig[]
/**
* Define all the workflows here. Workflows orchestrate the flow of multiple tasks.
*/
diff --git a/packages/payload/src/queues/operations/runJobs/runJob/getRunTaskFunction.ts b/packages/payload/src/queues/operations/runJobs/runJob/getRunTaskFunction.ts
index d5ff4d7c70..c0f22990b7 100644
--- a/packages/payload/src/queues/operations/runJobs/runJob/getRunTaskFunction.ts
+++ b/packages/payload/src/queues/operations/runJobs/runJob/getRunTaskFunction.ts
@@ -193,7 +193,9 @@ export const getRunTaskFunction = (
let taskConfig: TaskConfig
if (!isInline) {
- taskConfig = req.payload.config.jobs.tasks.find((t) => t.slug === taskSlug)
+ taskConfig =
+ req.payload.config.jobs.tasks?.length &&
+ req.payload.config.jobs.tasks.find((t) => t.slug === taskSlug)
if (!taskConfig) {
throw new Error(`Task ${taskSlug} not found in workflow ${job.workflowSlug}`)
diff --git a/packages/payload/src/queues/restEndpointRun.ts b/packages/payload/src/queues/restEndpointRun.ts
index edadcfc46e..888e161c34 100644
--- a/packages/payload/src/queues/restEndpointRun.ts
+++ b/packages/payload/src/queues/restEndpointRun.ts
@@ -1,4 +1,3 @@
-// @ts-strict-ignore
import type { Endpoint, SanitizedConfig } from '../config/types.js'
import { runJobs, type RunJobsArgs } from './operations/runJobs/index.js'
@@ -8,10 +7,10 @@ const configHasJobs = (config: SanitizedConfig): boolean => {
return false
}
- if (config.jobs.tasks.length > 0) {
+ if (config.jobs.tasks?.length > 0) {
return true
}
- if (Array.isArray(config.jobs.workflows) && config.jobs.workflows.length > 0) {
+ if (config.jobs.workflows?.length > 0) {
return true
}
@@ -61,7 +60,7 @@ export const runJobsEndpoint: Endpoint = {
let remainingJobsFromQueried = 0
try {
const result = await runJobs(runJobsArgs)
- noJobsRemaining = result.noJobsRemaining
+ noJobsRemaining = !!result.noJobsRemaining
remainingJobsFromQueried = result.remainingJobsFromQueried
} catch (err) {
req.payload.logger.error({
diff --git a/packages/payload/src/queues/utilities/getJobTaskStatus.ts b/packages/payload/src/queues/utilities/getJobTaskStatus.ts
index d911c42e53..44c49e2b81 100644
--- a/packages/payload/src/queues/utilities/getJobTaskStatus.ts
+++ b/packages/payload/src/queues/utilities/getJobTaskStatus.ts
@@ -1,10 +1,8 @@
// @ts-strict-ignore
-import type { TaskConfig, TaskType } from '../config/types/taskTypes.js'
import type { BaseJob, JobTaskStatus } from '../config/types/workflowTypes.js'
type Args = {
jobLog: BaseJob['log']
- tasksConfig: TaskConfig[]
}
export const getJobTaskStatus = ({ jobLog }: Args): JobTaskStatus => {
diff --git a/packages/payload/src/utilities/configToJSONSchema.ts b/packages/payload/src/utilities/configToJSONSchema.ts
index c00c95cbe6..daee3a0060 100644
--- a/packages/payload/src/utilities/configToJSONSchema.ts
+++ b/packages/payload/src/utilities/configToJSONSchema.ts
@@ -367,25 +367,26 @@ export function fieldsToJSONSchema(
break
}
- case 'group':
- case 'tab': {
- fieldSchema = {
- ...baseFieldSchema,
- type: 'object',
- additionalProperties: false,
- ...fieldsToJSONSchema(
- collectionIDFieldTypes,
- field.flattenedFields,
- interfaceNameDefinitions,
- config,
- i18n,
- ),
- }
+ case 'group': {
+ if (fieldAffectsData(field)) {
+ fieldSchema = {
+ ...baseFieldSchema,
+ type: 'object',
+ additionalProperties: false,
+ ...fieldsToJSONSchema(
+ collectionIDFieldTypes,
+ field.flattenedFields,
+ interfaceNameDefinitions,
+ config,
+ i18n,
+ ),
+ }
- if (field.interfaceName) {
- interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
+ if (field.interfaceName) {
+ interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
- fieldSchema = { $ref: `#/definitions/${field.interfaceName}` }
+ fieldSchema = { $ref: `#/definitions/${field.interfaceName}` }
+ }
}
break
}
@@ -486,6 +487,7 @@ export function fieldsToJSONSchema(
}
break
}
+
case 'radio': {
fieldSchema = {
...baseFieldSchema,
@@ -503,7 +505,6 @@ export function fieldsToJSONSchema(
break
}
-
case 'relationship':
case 'upload': {
if (Array.isArray(field.relationTo)) {
@@ -595,7 +596,6 @@ export function fieldsToJSONSchema(
break
}
-
case 'richText': {
if (!field?.editor) {
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
@@ -628,6 +628,7 @@ export function fieldsToJSONSchema(
break
}
+
case 'select': {
const optionEnums = buildOptionEnums(field.options)
// We get the previous field to check for a date in the case of a timezone select
@@ -675,6 +676,27 @@ export function fieldsToJSONSchema(
break
}
+ case 'tab': {
+ fieldSchema = {
+ ...baseFieldSchema,
+ type: 'object',
+ additionalProperties: false,
+ ...fieldsToJSONSchema(
+ collectionIDFieldTypes,
+ field.flattenedFields,
+ interfaceNameDefinitions,
+ config,
+ i18n,
+ ),
+ }
+
+ if (field.interfaceName) {
+ interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
+
+ fieldSchema = { $ref: `#/definitions/${field.interfaceName}` }
+ }
+ break
+ }
case 'text':
if (field.hasMany === true) {
diff --git a/packages/payload/src/utilities/fieldSchemaToJSON.ts b/packages/payload/src/utilities/fieldSchemaToJSON.ts
index 0b94e9f1eb..2f0e285197 100644
--- a/packages/payload/src/utilities/fieldSchemaToJSON.ts
+++ b/packages/payload/src/utilities/fieldSchemaToJSON.ts
@@ -1,7 +1,8 @@
import type { ClientConfig } from '../config/client.js'
// @ts-strict-ignore
import type { ClientField } from '../fields/config/client.js'
-import type { FieldTypes } from '../fields/config/types.js'
+
+import { fieldAffectsData, type FieldTypes } from '../fields/config/types.js'
export type FieldSchemaJSON = {
blocks?: FieldSchemaJSON // TODO: conditionally add based on `type`
@@ -67,11 +68,15 @@ export const fieldSchemaToJSON = (fields: ClientField[], config: ClientConfig):
break
case 'group':
- acc.push({
- name: field.name,
- type: field.type,
- fields: fieldSchemaToJSON(field.fields, config),
- })
+ if (fieldAffectsData(field)) {
+ acc.push({
+ name: field.name,
+ type: field.type,
+ fields: fieldSchemaToJSON(field.fields, config),
+ })
+ } else {
+ result = result.concat(fieldSchemaToJSON(field.fields, config))
+ }
break
diff --git a/packages/payload/src/utilities/flattenAllFields.ts b/packages/payload/src/utilities/flattenAllFields.ts
index 97fa0b5dd3..9173e0885a 100644
--- a/packages/payload/src/utilities/flattenAllFields.ts
+++ b/packages/payload/src/utilities/flattenAllFields.ts
@@ -7,7 +7,7 @@ import type {
FlattenedJoinField,
} from '../fields/config/types.js'
-import { tabHasName } from '../fields/config/types.js'
+import { fieldAffectsData, tabHasName } from '../fields/config/types.js'
export const flattenBlock = ({ block }: { block: Block }): FlattenedBlock => {
return {
@@ -44,7 +44,13 @@ export const flattenAllFields = ({
switch (field.type) {
case 'array':
case 'group': {
- result.push({ ...field, flattenedFields: flattenAllFields({ fields: field.fields }) })
+ if (fieldAffectsData(field)) {
+ result.push({ ...field, flattenedFields: flattenAllFields({ fields: field.fields }) })
+ } else {
+ for (const nestedField of flattenAllFields({ fields: field.fields })) {
+ result.push(nestedField)
+ }
+ }
break
}
diff --git a/packages/payload/src/utilities/getEntityPolicies.ts b/packages/payload/src/utilities/getEntityPolicies.ts
index 104a6ead84..0ec0538b6e 100644
--- a/packages/payload/src/utilities/getEntityPolicies.ts
+++ b/packages/payload/src/utilities/getEntityPolicies.ts
@@ -78,6 +78,7 @@ export async function getEntityPolicies(args: T): Promise(args: T): Promise(args: T): Promise) {
- if (currentRef[key]) {
- traverseFields({
- callback,
- callbackStack,
- config,
- fields: field.fields,
- fillEmpty,
- isTopLevel: false,
- leavesFirst,
- parentIsLocalized: true,
- parentRef: currentParentRef,
- ref: currentRef[key],
- })
+ if (fieldAffectsData(field)) {
+ for (const key in currentRef as Record) {
+ if (currentRef[key]) {
+ traverseFields({
+ callback,
+ callbackStack,
+ config,
+ fields: field.fields,
+ fillEmpty,
+ isTopLevel: false,
+ leavesFirst,
+ parentIsLocalized: true,
+ parentRef: currentParentRef,
+ ref: currentRef[key],
+ })
+ }
}
+ } else {
+ traverseFields({
+ callback,
+ callbackStack,
+ config,
+ fields: field.fields,
+ fillEmpty,
+ isTopLevel: false,
+ leavesFirst,
+ parentIsLocalized,
+ parentRef: currentParentRef,
+ ref: currentRef,
+ })
}
+
return
}
diff --git a/packages/plugin-cloud-storage/package.json b/packages/plugin-cloud-storage/package.json
index 8b5d9020f9..c292b0f7be 100644
--- a/packages/plugin-cloud-storage/package.json
+++ b/packages/plugin-cloud-storage/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud-storage",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "The official cloud storage plugin for Payload CMS",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/plugin-cloud-storage/src/utilities/getFilePrefix.ts b/packages/plugin-cloud-storage/src/utilities/getFilePrefix.ts
index 6e2771f51d..e823a6d861 100644
--- a/packages/plugin-cloud-storage/src/utilities/getFilePrefix.ts
+++ b/packages/plugin-cloud-storage/src/utilities/getFilePrefix.ts
@@ -26,6 +26,7 @@ export async function getFilePrefix({
const files = await req.payload.find({
collection: collection.slug,
depth: 0,
+ draft: true,
limit: 1,
pagination: false,
where: {
diff --git a/packages/plugin-form-builder/package.json b/packages/plugin-form-builder/package.json
index bf5de206ac..d6ca17d493 100644
--- a/packages/plugin-form-builder/package.json
+++ b/packages/plugin-form-builder/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",
diff --git a/packages/plugin-import-export/package.json b/packages/plugin-import-export/package.json
index 0317874ffc..bb1356d6ac 100644
--- a/packages/plugin-import-export/package.json
+++ b/packages/plugin-import-export/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-import-export",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Import-Export plugin for Payload",
"keywords": [
"payload",
diff --git a/packages/plugin-multi-tenant/package.json b/packages/plugin-multi-tenant/package.json
index 077b46d5b1..43029726bd 100644
--- a/packages/plugin-multi-tenant/package.json
+++ b/packages/plugin-multi-tenant/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-multi-tenant",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Multi Tenant plugin for Payload",
"keywords": [
"payload",
@@ -56,6 +56,16 @@
"import": "./src/exports/utilities.ts",
"types": "./src/exports/utilities.ts",
"default": "./src/exports/utilities.ts"
+ },
+ "./translations/languages/all": {
+ "import": "./src/translations/index.ts",
+ "types": "./src/translations/index.ts",
+ "default": "./src/translations/index.ts"
+ },
+ "./translations/languages/*": {
+ "import": "./src/translations/languages/*.ts",
+ "types": "./src/translations/languages/*.ts",
+ "default": "./src/translations/languages/*.ts"
}
},
"main": "./src/index.ts",
@@ -118,6 +128,16 @@
"import": "./dist/exports/utilities.js",
"types": "./dist/exports/utilities.d.ts",
"default": "./dist/exports/utilities.js"
+ },
+ "./translations/languages/all": {
+ "import": "./dist/translations/index.js",
+ "types": "./dist/translations/index.d.ts",
+ "default": "./dist/translations/index.js"
+ },
+ "./translations/languages/*": {
+ "import": "./dist/translations/languages/*.js",
+ "types": "./dist/translations/languages/*.d.ts",
+ "default": "./dist/translations/languages/*.js"
}
},
"main": "./dist/index.js",
diff --git a/packages/plugin-multi-tenant/src/components/TenantSelector/index.tsx b/packages/plugin-multi-tenant/src/components/TenantSelector/index.tsx
index 38d11df84a..16cba01693 100644
--- a/packages/plugin-multi-tenant/src/components/TenantSelector/index.tsx
+++ b/packages/plugin-multi-tenant/src/components/TenantSelector/index.tsx
@@ -3,18 +3,52 @@ import type { ReactSelectOption } from '@payloadcms/ui'
import type { ViewTypes } from 'payload'
import { getTranslation } from '@payloadcms/translations'
-import { SelectInput, useTranslation } from '@payloadcms/ui'
+import {
+ ConfirmationModal,
+ SelectInput,
+ Translation,
+ useModal,
+ useTranslation,
+} from '@payloadcms/ui'
import React from 'react'
+import type {
+ PluginMultiTenantTranslationKeys,
+ PluginMultiTenantTranslations,
+} from '../../translations/index.js'
+
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
import './index.scss'
-export const TenantSelector = ({ label, viewType }: { label: string; viewType?: ViewTypes }) => {
- const { options, selectedTenantID, setTenant } = useTenantSelection()
- const { i18n } = useTranslation()
+const confirmSwitchTenantSlug = 'confirmSwitchTenant'
- const handleChange = React.useCallback(
- (option: ReactSelectOption | ReactSelectOption[]) => {
+export const TenantSelector = ({ label, viewType }: { label: string; viewType?: ViewTypes }) => {
+ const { options, preventRefreshOnChange, selectedTenantID, setTenant } = useTenantSelection()
+ const { openModal } = useModal()
+ const { i18n, t } = useTranslation<
+ PluginMultiTenantTranslations,
+ PluginMultiTenantTranslationKeys
+ >()
+ const [tenantSelection, setTenantSelection] = React.useState<
+ ReactSelectOption | ReactSelectOption[]
+ >()
+
+ const selectedValue = React.useMemo(() => {
+ if (selectedTenantID) {
+ return options.find((option) => option.value === selectedTenantID)
+ }
+ return undefined
+ }, [options, selectedTenantID])
+
+ const newSelectedValue = React.useMemo(() => {
+ if (tenantSelection && 'value' in tenantSelection) {
+ return options.find((option) => option.value === tenantSelection.value)
+ }
+ return undefined
+ }, [options, tenantSelection])
+
+ const switchTenant = React.useCallback(
+ (option: ReactSelectOption | ReactSelectOption[] | undefined) => {
if (option && 'value' in option) {
setTenant({ id: option.value as string, refresh: true })
} else {
@@ -24,6 +58,19 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
[setTenant],
)
+ const onChange = React.useCallback(
+ (option: ReactSelectOption | ReactSelectOption[]) => {
+ if (!preventRefreshOnChange) {
+ switchTenant(option)
+ return
+ } else {
+ setTenantSelection(option)
+ openModal(confirmSwitchTenantSlug)
+ }
+ },
+ [openModal, preventRefreshOnChange, switchTenant],
+ )
+
if (options.length <= 1) {
return null
}
@@ -34,11 +81,46 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
isClearable={viewType === 'list'}
label={getTranslation(label, i18n)}
name="setTenant"
- onChange={handleChange}
+ onChange={onChange}
options={options}
path="setTenant"
value={selectedTenantID as string | undefined}
/>
+
+ {
+ return {children}
+ },
+ }}
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ i18nKey="plugin-multi-tenant:confirm-tenant-switch--body"
+ t={t}
+ variables={{
+ fromTenant: selectedValue?.label,
+ toTenant: newSelectedValue?.label,
+ }}
+ />
+ }
+ heading={
+
+ }
+ modalSlug={confirmSwitchTenantSlug}
+ onConfirm={() => {
+ switchTenant(tenantSelection)
+ }}
+ />
)
}
diff --git a/packages/plugin-multi-tenant/src/components/WatchTenantCollection/index.tsx b/packages/plugin-multi-tenant/src/components/WatchTenantCollection/index.tsx
new file mode 100644
index 0000000000..b20a559b31
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/components/WatchTenantCollection/index.tsx
@@ -0,0 +1,35 @@
+'use client'
+
+import type { ClientCollectionConfig } from 'payload'
+
+import { useConfig, useDocumentInfo, useEffectEvent, useFormFields } from '@payloadcms/ui'
+import React from 'react'
+
+import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
+
+export const WatchTenantCollection = () => {
+ const { id, collectionSlug, title } = useDocumentInfo()
+ const { getEntityConfig } = useConfig()
+ const [useAsTitleName] = React.useState(
+ () => (getEntityConfig({ collectionSlug }) as ClientCollectionConfig).admin.useAsTitle,
+ )
+ const titleField = useFormFields(([fields]) => fields[useAsTitleName])
+
+ const { updateTenants } = useTenantSelection()
+
+ const syncTenantTitle = useEffectEvent(() => {
+ if (id) {
+ updateTenants({ id, label: title })
+ }
+ })
+
+ React.useEffect(() => {
+ // only update the tenant selector when the document saves
+ // → aka when initial value changes
+ if (id && titleField?.initialValue) {
+ syncTenantTitle()
+ }
+ }, [id, titleField?.initialValue])
+
+ return null
+}
diff --git a/packages/plugin-multi-tenant/src/exports/client.ts b/packages/plugin-multi-tenant/src/exports/client.ts
index cd6fb65b94..d14df7a70c 100644
--- a/packages/plugin-multi-tenant/src/exports/client.ts
+++ b/packages/plugin-multi-tenant/src/exports/client.ts
@@ -1,3 +1,4 @@
export { TenantField } from '../components/TenantField/index.client.js'
export { TenantSelector } from '../components/TenantSelector/index.js'
+export { WatchTenantCollection } from '../components/WatchTenantCollection/index.js'
export { useTenantSelection } from '../providers/TenantSelectionProvider/index.client.js'
diff --git a/packages/plugin-multi-tenant/src/index.ts b/packages/plugin-multi-tenant/src/index.ts
index 6d5bf30859..4b47f24594 100644
--- a/packages/plugin-multi-tenant/src/index.ts
+++ b/packages/plugin-multi-tenant/src/index.ts
@@ -1,6 +1,9 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import type { CollectionConfig, Config } from 'payload'
+import { deepMergeSimple } from 'payload'
+
+import type { PluginDefaultTranslationsObject } from './translations/types.js'
import type { MultiTenantPluginConfig } from './types.js'
import { defaults } from './defaults.js'
@@ -10,6 +13,7 @@ import { addTenantCleanup } from './hooks/afterTenantDelete.js'
import { filterDocumentsBySelectedTenant } from './list-filters/filterDocumentsBySelectedTenant.js'
import { filterTenantsBySelectedTenant } from './list-filters/filterTenantsBySelectedTenant.js'
import { filterUsersBySelectedTenant } from './list-filters/filterUsersBySelectedTenant.js'
+import { translations } from './translations/index.js'
import { addCollectionAccess } from './utilities/addCollectionAccess.js'
import { addFilterOptionsToFields } from './utilities/addFilterOptionsToFields.js'
import { combineListFilters } from './utilities/combineListFilters.js'
@@ -229,6 +233,21 @@ export const multiTenantPlugin =
usersTenantsArrayTenantFieldName: tenantsArrayTenantFieldName,
})
}
+
+ /**
+ * Add custom tenant field that watches and dispatches updates to the selector
+ */
+ collection.fields.push({
+ name: '_watchTenant',
+ type: 'ui',
+ admin: {
+ components: {
+ Field: {
+ path: '@payloadcms/plugin-multi-tenant/client#WatchTenantCollection',
+ },
+ },
+ },
+ })
} else if (pluginConfig.collections?.[collection.slug]) {
const isGlobal = Boolean(pluginConfig.collections[collection.slug]?.isGlobal)
@@ -340,5 +359,25 @@ export const multiTenantPlugin =
path: '@payloadcms/plugin-multi-tenant/client#TenantSelector',
})
+ /**
+ * Merge plugin translations
+ */
+
+ const simplifiedTranslations = Object.entries(translations).reduce(
+ (acc, [key, value]) => {
+ acc[key] = value.translations
+ return acc
+ },
+ {} as Record,
+ )
+
+ incomingConfig.i18n = {
+ ...incomingConfig.i18n,
+ translations: deepMergeSimple(
+ simplifiedTranslations,
+ incomingConfig.i18n?.translations ?? {},
+ ),
+ }
+
return incomingConfig
}
diff --git a/packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.client.tsx b/packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.client.tsx
index 4aad06e231..be7828e36b 100644
--- a/packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.client.tsx
+++ b/packages/plugin-multi-tenant/src/providers/TenantSelectionProvider/index.client.tsx
@@ -11,6 +11,7 @@ type ContextType = {
* Array of options to select from
*/
options: OptionObject[]
+ preventRefreshOnChange: boolean
/**
* The currently selected tenant ID
*/
@@ -28,20 +29,26 @@ type ContextType = {
* @param args.refresh - Whether to refresh the page after changing the tenant
*/
setTenant: (args: { id: number | string | undefined; refresh?: boolean }) => void
+ /**
+ *
+ */
+ updateTenants: (args: { id: number | string; label: string }) => void
}
const Context = createContext({
options: [],
+ preventRefreshOnChange: false,
selectedTenantID: undefined,
setPreventRefreshOnChange: () => null,
setTenant: () => null,
+ updateTenants: () => null,
})
export const TenantSelectionProviderClient = ({
children,
initialValue,
tenantCookie,
- tenantOptions,
+ tenantOptions: tenantOptionsFromProps,
}: {
children: React.ReactNode
initialValue?: number | string
@@ -54,6 +61,9 @@ export const TenantSelectionProviderClient = ({
const [preventRefreshOnChange, setPreventRefreshOnChange] = React.useState(false)
const { user } = useAuth()
const userID = React.useMemo(() => user?.id, [user?.id])
+ const [tenantOptions, setTenantOptions] = React.useState(
+ () => tenantOptionsFromProps,
+ )
const selectedTenantLabel = React.useMemo(
() => tenantOptions.find((option) => option.value === selectedTenantID)?.label,
[selectedTenantID, tenantOptions],
@@ -91,6 +101,20 @@ export const TenantSelectionProviderClient = ({
[deleteCookie, preventRefreshOnChange, router, setCookie, setSelectedTenantID, tenantOptions],
)
+ const updateTenants = React.useCallback(({ id, label }) => {
+ setTenantOptions((prev) => {
+ return prev.map((currentTenant) => {
+ if (id === currentTenant.value) {
+ return {
+ label,
+ value: id,
+ }
+ }
+ return currentTenant
+ })
+ })
+ }, [])
+
React.useEffect(() => {
if (selectedTenantID && !tenantOptions.find((option) => option.value === selectedTenantID)) {
if (tenantOptions?.[0]?.value) {
@@ -105,13 +129,14 @@ export const TenantSelectionProviderClient = ({
if (userID && !tenantCookie) {
// User is logged in, but does not have a tenant cookie, set it
setSelectedTenantID(initialValue)
+ setTenantOptions(tenantOptionsFromProps)
if (initialValue) {
setCookie(String(initialValue))
} else {
deleteCookie()
}
}
- }, [userID, tenantCookie, initialValue, setCookie, deleteCookie, router])
+ }, [userID, tenantCookie, initialValue, setCookie, deleteCookie, router, tenantOptionsFromProps])
React.useEffect(() => {
if (!userID && tenantCookie) {
@@ -132,9 +157,11 @@ export const TenantSelectionProviderClient = ({
{children}
diff --git a/packages/plugin-multi-tenant/src/queries/findTenantOptions.ts b/packages/plugin-multi-tenant/src/queries/findTenantOptions.ts
index 3d41343cc8..ae98cbe6a3 100644
--- a/packages/plugin-multi-tenant/src/queries/findTenantOptions.ts
+++ b/packages/plugin-multi-tenant/src/queries/findTenantOptions.ts
@@ -14,6 +14,7 @@ export const findTenantOptions = async ({
useAsTitle,
user,
}: Args): Promise => {
+ const isOrderable = payload.collections[tenantsCollectionSlug]?.config?.orderable || false
return payload.find({
collection: tenantsCollectionSlug,
depth: 0,
@@ -21,8 +22,9 @@ export const findTenantOptions = async ({
overrideAccess: false,
select: {
[useAsTitle]: true,
+ ...(isOrderable ? { _order: true } : {}),
},
- sort: useAsTitle,
+ sort: isOrderable ? '_order' : useAsTitle,
user,
})
}
diff --git a/packages/plugin-multi-tenant/src/translations/index.ts b/packages/plugin-multi-tenant/src/translations/index.ts
new file mode 100644
index 0000000000..f8e7816818
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/index.ts
@@ -0,0 +1,91 @@
+import type {
+ GenericTranslationsObject,
+ NestedKeysStripped,
+ SupportedLanguages,
+} from '@payloadcms/translations'
+
+import type { PluginDefaultTranslationsObject } from './types.js'
+
+import { ar } from './languages/ar.js'
+import { az } from './languages/az.js'
+import { bg } from './languages/bg.js'
+import { ca } from './languages/ca.js'
+import { cs } from './languages/cs.js'
+import { da } from './languages/da.js'
+import { de } from './languages/de.js'
+import { en } from './languages/en.js'
+import { es } from './languages/es.js'
+import { et } from './languages/et.js'
+import { fa } from './languages/fa.js'
+import { fr } from './languages/fr.js'
+import { he } from './languages/he.js'
+import { hr } from './languages/hr.js'
+import { hu } from './languages/hu.js'
+import { hy } from './languages/hy.js'
+import { it } from './languages/it.js'
+import { ja } from './languages/ja.js'
+import { ko } from './languages/ko.js'
+import { lt } from './languages/lt.js'
+import { my } from './languages/my.js'
+import { nb } from './languages/nb.js'
+import { nl } from './languages/nl.js'
+import { pl } from './languages/pl.js'
+import { pt } from './languages/pt.js'
+import { ro } from './languages/ro.js'
+import { rs } from './languages/rs.js'
+import { rsLatin } from './languages/rsLatin.js'
+import { ru } from './languages/ru.js'
+import { sk } from './languages/sk.js'
+import { sl } from './languages/sl.js'
+import { sv } from './languages/sv.js'
+import { th } from './languages/th.js'
+import { tr } from './languages/tr.js'
+import { uk } from './languages/uk.js'
+import { vi } from './languages/vi.js'
+import { zh } from './languages/zh.js'
+import { zhTw } from './languages/zhTw.js'
+
+export const translations = {
+ ar,
+ az,
+ bg,
+ ca,
+ cs,
+ da,
+ de,
+ en,
+ es,
+ et,
+ fa,
+ fr,
+ he,
+ hr,
+ hu,
+ hy,
+ it,
+ ja,
+ ko,
+ lt,
+ my,
+ nb,
+ nl,
+ pl,
+ pt,
+ ro,
+ rs,
+ 'rs-latin': rsLatin,
+ ru,
+ sk,
+ sl,
+ sv,
+ th,
+ tr,
+ uk,
+ vi,
+ zh,
+ 'zh-TW': zhTw,
+} as SupportedLanguages
+
+export type PluginMultiTenantTranslations = GenericTranslationsObject
+
+export type PluginMultiTenantTranslationKeys = NestedKeysStripped
diff --git a/packages/plugin-multi-tenant/src/translations/languages/ar.ts b/packages/plugin-multi-tenant/src/translations/languages/ar.ts
new file mode 100644
index 0000000000..ff50feeb56
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/ar.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const arTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'أنت على وشك تغيير الملكية من <0>{{fromTenant}}0> إلى <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'تأكيد تغيير {{tenantLabel}}',
+ },
+}
+
+export const ar: PluginLanguage = {
+ dateFNSKey: 'ar',
+ translations: arTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/az.ts b/packages/plugin-multi-tenant/src/translations/languages/az.ts
new file mode 100644
index 0000000000..3c7ace19b4
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/az.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const azTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Siz <0>{{fromTenant}}0> mülkiyyətini <0>{{toTenant}}0> mülkiyyətinə dəyişdirəcəksiniz.',
+ 'confirm-tenant-switch--heading': '{{tenantLabel}} dəyişikliyini təsdiqləyin',
+ },
+}
+
+export const az: PluginLanguage = {
+ dateFNSKey: 'az',
+ translations: azTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/bg.ts b/packages/plugin-multi-tenant/src/translations/languages/bg.ts
new file mode 100644
index 0000000000..488b0b9b1a
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/bg.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const bgTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Предстои да промените собствеността от <0>{{fromTenant}}0> на <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Потвърдете промяната на {{tenantLabel}}',
+ },
+}
+
+export const bg: PluginLanguage = {
+ dateFNSKey: 'bg',
+ translations: bgTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/ca.ts b/packages/plugin-multi-tenant/src/translations/languages/ca.ts
new file mode 100644
index 0000000000..0de5a291cf
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/ca.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const caTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Estàs a punt de canviar la propietat de <0>{{fromTenant}}0> a <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Confirmeu el canvi de {{tenantLabel}}',
+ },
+}
+
+export const ca: PluginLanguage = {
+ dateFNSKey: 'ca',
+ translations: caTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/cs.ts b/packages/plugin-multi-tenant/src/translations/languages/cs.ts
new file mode 100644
index 0000000000..30d024ad51
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/cs.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const csTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Chystáte se změnit vlastnictví z <0>{{fromTenant}}0> na <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Potvrďte změnu {{tenantLabel}}',
+ },
+}
+
+export const cs: PluginLanguage = {
+ dateFNSKey: 'cs',
+ translations: csTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/da.ts b/packages/plugin-multi-tenant/src/translations/languages/da.ts
new file mode 100644
index 0000000000..a7c3898058
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/da.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const daTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Du er ved at ændre ejerskab fra <0>{{fromTenant}}0> til <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Bekræft {{tenantLabel}} ændring',
+ },
+}
+
+export const da: PluginLanguage = {
+ dateFNSKey: 'da',
+ translations: daTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/de.ts b/packages/plugin-multi-tenant/src/translations/languages/de.ts
new file mode 100644
index 0000000000..d36e28346a
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/de.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const deTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Sie sind dabei, den Besitz von <0>{{fromTenant}}0> auf <0>{{toTenant}}0> zu übertragen.',
+ 'confirm-tenant-switch--heading': 'Bestätigen Sie die Änderung von {{tenantLabel}}.',
+ },
+}
+
+export const de: PluginLanguage = {
+ dateFNSKey: 'de',
+ translations: deTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/en.ts b/packages/plugin-multi-tenant/src/translations/languages/en.ts
new file mode 100644
index 0000000000..4e790e89d8
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/en.ts
@@ -0,0 +1,14 @@
+import type { PluginLanguage } from '../types.js'
+
+export const enTranslations = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'You are about to change ownership from <0>{{fromTenant}}0> to <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Confirm {{tenantLabel}} change',
+ },
+}
+
+export const en: PluginLanguage = {
+ dateFNSKey: 'en-US',
+ translations: enTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/es.ts b/packages/plugin-multi-tenant/src/translations/languages/es.ts
new file mode 100644
index 0000000000..4e72ff9d0b
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/es.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const esTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Está a punto de cambiar la propiedad de <0>{{fromTenant}}0> a <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Confirme el cambio de {{tenantLabel}}',
+ },
+}
+
+export const es: PluginLanguage = {
+ dateFNSKey: 'es',
+ translations: esTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/et.ts b/packages/plugin-multi-tenant/src/translations/languages/et.ts
new file mode 100644
index 0000000000..1b86bf72a5
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/et.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const etTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Te olete tegemas omandiõiguse muudatust <0>{{fromTenant}}0>lt <0>{{toTenant}}0>le.',
+ 'confirm-tenant-switch--heading': 'Kinnita {{tenantLabel}} muutus',
+ },
+}
+
+export const et: PluginLanguage = {
+ dateFNSKey: 'et',
+ translations: etTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/fa.ts b/packages/plugin-multi-tenant/src/translations/languages/fa.ts
new file mode 100644
index 0000000000..d64610a729
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/fa.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const faTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'شما در حال تغییر مالکیت از <0>{{fromTenant}}0> به <0>{{toTenant}}0> هستید',
+ 'confirm-tenant-switch--heading': 'تایید تغییر {{tenantLabel}}',
+ },
+}
+
+export const fa: PluginLanguage = {
+ dateFNSKey: 'fa-IR',
+ translations: faTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/fr.ts b/packages/plugin-multi-tenant/src/translations/languages/fr.ts
new file mode 100644
index 0000000000..720cb3d797
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/fr.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const frTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Vous êtes sur le point de changer la propriété de <0>{{fromTenant}}0> à <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Confirmer le changement de {{tenantLabel}}',
+ },
+}
+
+export const fr: PluginLanguage = {
+ dateFNSKey: 'fr',
+ translations: frTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/he.ts b/packages/plugin-multi-tenant/src/translations/languages/he.ts
new file mode 100644
index 0000000000..1a522ec86f
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/he.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const heTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'אתה עומד לשנות בעלות מ- <0>{{fromTenant}}0> ל- <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'אשר שינוי {{tenantLabel}}',
+ },
+}
+
+export const he: PluginLanguage = {
+ dateFNSKey: 'he',
+ translations: heTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/hr.ts b/packages/plugin-multi-tenant/src/translations/languages/hr.ts
new file mode 100644
index 0000000000..292ec35218
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/hr.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const hrTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Upravo ćete promijeniti vlasništvo sa <0>{{fromTenant}}0> na <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Potvrdi promjenu {{tenantLabel}}',
+ },
+}
+
+export const hr: PluginLanguage = {
+ dateFNSKey: 'hr',
+ translations: hrTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/hu.ts b/packages/plugin-multi-tenant/src/translations/languages/hu.ts
new file mode 100644
index 0000000000..5e7a03bf2e
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/hu.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const huTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Ön azon van, hogy megváltoztassa a tulajdonjogot <0>{{fromTenant}}0>-ről <0>{{toTenant}}0>-re.',
+ 'confirm-tenant-switch--heading': 'Erősítse meg a(z) {{tenantLabel}} változtatást',
+ },
+}
+
+export const hu: PluginLanguage = {
+ dateFNSKey: 'hu',
+ translations: huTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/hy.ts b/packages/plugin-multi-tenant/src/translations/languages/hy.ts
new file mode 100644
index 0000000000..e46f11ac5f
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/hy.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const hyTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Դուք պատրաստ եք փոխել գերեցդիմատնին ընկերությունը <0>{{fromTenant}}0>-ից <0>{{toTenant}}0>-ին',
+ 'confirm-tenant-switch--heading': 'Հաստատեք {{tenantLabel}} փոփոխությունը',
+ },
+}
+
+export const hy: PluginLanguage = {
+ dateFNSKey: 'hy-AM',
+ translations: hyTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/it.ts b/packages/plugin-multi-tenant/src/translations/languages/it.ts
new file mode 100644
index 0000000000..af0ce98a75
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/it.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const itTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Stai per cambiare proprietà da <0>{{fromTenant}}0> a <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Conferma il cambiamento di {{tenantLabel}}',
+ },
+}
+
+export const it: PluginLanguage = {
+ dateFNSKey: 'it',
+ translations: itTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/ja.ts b/packages/plugin-multi-tenant/src/translations/languages/ja.ts
new file mode 100644
index 0000000000..23adbe4131
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/ja.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const jaTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'あなたは所有権を<0>{{fromTenant}}0>から<0>{{toTenant}}0>へ変更しようとしています',
+ 'confirm-tenant-switch--heading': '{{tenantLabel}}の変更を確認してください',
+ },
+}
+
+export const ja: PluginLanguage = {
+ dateFNSKey: 'ja',
+ translations: jaTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/ko.ts b/packages/plugin-multi-tenant/src/translations/languages/ko.ts
new file mode 100644
index 0000000000..7836ab8f9e
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/ko.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const koTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ '<0>{{fromTenant}}0>에서 <0>{{toTenant}}0>으로 소유권을 변경하려고 합니다.',
+ 'confirm-tenant-switch--heading': '{{tenantLabel}} 변경을 확인하세요',
+ },
+}
+
+export const ko: PluginLanguage = {
+ dateFNSKey: 'ko',
+ translations: koTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/lt.ts b/packages/plugin-multi-tenant/src/translations/languages/lt.ts
new file mode 100644
index 0000000000..396855ada9
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/lt.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const ltTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Jūs ketinate pakeisti nuosavybės teisę iš <0>{{fromTenant}}0> į <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Patvirtinkite {{tenantLabel}} pakeitimą',
+ },
+}
+
+export const lt: PluginLanguage = {
+ dateFNSKey: 'lt',
+ translations: ltTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/my.ts b/packages/plugin-multi-tenant/src/translations/languages/my.ts
new file mode 100644
index 0000000000..ae1a6349b6
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/my.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const myTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Anda akan mengubah pemilikan dari <0>{{fromTenant}}0> ke <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Sahkan perubahan {{tenantLabel}}',
+ },
+}
+
+export const my: PluginLanguage = {
+ dateFNSKey: 'en-US',
+ translations: myTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/nb.ts b/packages/plugin-multi-tenant/src/translations/languages/nb.ts
new file mode 100644
index 0000000000..c7f85e270d
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/nb.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const nbTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Du er i ferd med å endre eierskap fra <0>{{fromTenant}}0> til <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Bekreft {{tenantLabel}} endring',
+ },
+}
+
+export const nb: PluginLanguage = {
+ dateFNSKey: 'nb',
+ translations: nbTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/nl.ts b/packages/plugin-multi-tenant/src/translations/languages/nl.ts
new file mode 100644
index 0000000000..21ee10b6d0
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/nl.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const nlTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'U staat op het punt het eigendom te wijzigen van <0>{{fromTenant}}0> naar <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Bevestig wijziging van {{tenantLabel}}',
+ },
+}
+
+export const nl: PluginLanguage = {
+ dateFNSKey: 'nl',
+ translations: nlTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/pl.ts b/packages/plugin-multi-tenant/src/translations/languages/pl.ts
new file mode 100644
index 0000000000..4f1a732246
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/pl.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const plTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Za chwilę nastąpi zmiana właściciela z <0>{{fromTenant}}0> na <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Potwierdź zmianę {{tenantLabel}}',
+ },
+}
+
+export const pl: PluginLanguage = {
+ dateFNSKey: 'pl',
+ translations: plTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/pt.ts b/packages/plugin-multi-tenant/src/translations/languages/pt.ts
new file mode 100644
index 0000000000..1e52af5ec7
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/pt.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const ptTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Você está prestes a alterar a propriedade de <0>{{fromTenant}}0> para <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Confirme a alteração de {{tenantLabel}}',
+ },
+}
+
+export const pt: PluginLanguage = {
+ dateFNSKey: 'pt',
+ translations: ptTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/rn.sh b/packages/plugin-multi-tenant/src/translations/languages/rn.sh
new file mode 100644
index 0000000000..ef6b3ed6c4
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/rn.sh
@@ -0,0 +1,3 @@
+for file in *.js; do
+ mv -- "$file" "${file%.js}.ts"
+done
diff --git a/packages/plugin-multi-tenant/src/translations/languages/ro.ts b/packages/plugin-multi-tenant/src/translations/languages/ro.ts
new file mode 100644
index 0000000000..b6a40cd36f
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/ro.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const roTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Sunteți pe punctul de a schimba proprietatea de la <0>{{fromTenant}}0> la <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Confirmați schimbarea {{tenantLabel}}',
+ },
+}
+
+export const ro: PluginLanguage = {
+ dateFNSKey: 'ro',
+ translations: roTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/rs.ts b/packages/plugin-multi-tenant/src/translations/languages/rs.ts
new file mode 100644
index 0000000000..19c63dc59e
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/rs.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const rsTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Upravo ćete promeniti vlasništvo sa <0>{{fromTenant}}0> na <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Potvrdi promena {{tenantLabel}}',
+ },
+}
+
+export const rs: PluginLanguage = {
+ dateFNSKey: 'rs',
+ translations: rsTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/rsLatin.ts b/packages/plugin-multi-tenant/src/translations/languages/rsLatin.ts
new file mode 100644
index 0000000000..c4f56096f0
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/rsLatin.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const rsLatinTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Uskoro ćete promeniti vlasništvo sa <0>{{fromTenant}}0> na <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Potvrdite promenu {{tenantLabel}}',
+ },
+}
+
+export const rsLatin: PluginLanguage = {
+ dateFNSKey: 'rs-Latin',
+ translations: rsLatinTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/ru.ts b/packages/plugin-multi-tenant/src/translations/languages/ru.ts
new file mode 100644
index 0000000000..66bfaa76cb
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/ru.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const ruTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Вы собираетесь изменить владельца с <0>{{fromTenant}}0> на <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Подтвердите изменение {{tenantLabel}}',
+ },
+}
+
+export const ru: PluginLanguage = {
+ dateFNSKey: 'ru',
+ translations: ruTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/sk.ts b/packages/plugin-multi-tenant/src/translations/languages/sk.ts
new file mode 100644
index 0000000000..22148c870f
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/sk.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const skTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Chystáte sa zmeniť vlastníctvo z <0>{{fromTenant}}0> na <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Potvrďte zmenu {{tenantLabel}}',
+ },
+}
+
+export const sk: PluginLanguage = {
+ dateFNSKey: 'sk',
+ translations: skTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/sl.ts b/packages/plugin-multi-tenant/src/translations/languages/sl.ts
new file mode 100644
index 0000000000..c2f6bb2bf6
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/sl.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const slTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Ravno ste pred spremembo lastništva iz <0>{{fromTenant}}0> na <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Potrdi spremembo {{tenantLabel}}',
+ },
+}
+
+export const sl: PluginLanguage = {
+ dateFNSKey: 'sl-SI',
+ translations: slTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/sv.ts b/packages/plugin-multi-tenant/src/translations/languages/sv.ts
new file mode 100644
index 0000000000..fcf4972ff5
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/sv.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const svTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Du är på väg att ändra ägare från <0>{{fromTenant}}0> till <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Bekräfta ändring av {{tenantLabel}}',
+ },
+}
+
+export const sv: PluginLanguage = {
+ dateFNSKey: 'sv',
+ translations: svTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/th.ts b/packages/plugin-multi-tenant/src/translations/languages/th.ts
new file mode 100644
index 0000000000..cdfdc45c9a
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/th.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const thTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'คุณกำลังจะเปลี่ยนความเป็นเจ้าของจาก <0>{{fromTenant}}0> เป็น <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'ยืนยันการเปลี่ยนแปลง {{tenantLabel}}',
+ },
+}
+
+export const th: PluginLanguage = {
+ dateFNSKey: 'th',
+ translations: thTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/tr.ts b/packages/plugin-multi-tenant/src/translations/languages/tr.ts
new file mode 100644
index 0000000000..2f969402e2
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/tr.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const trTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ "Sahipliği <0>{{fromTenant}}0>'den <0>{{toTenant}}0>'e değiştirmek üzeresiniz.",
+ 'confirm-tenant-switch--heading': '{{tenantLabel}} değişikliğini onayla',
+ },
+}
+
+export const tr: PluginLanguage = {
+ dateFNSKey: 'tr',
+ translations: trTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/uk.ts b/packages/plugin-multi-tenant/src/translations/languages/uk.ts
new file mode 100644
index 0000000000..8e0fa89233
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/uk.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const ukTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Ви збираєтесь змінити власність з <0>{{fromTenant}}0> на <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Підтвердіть зміну {{tenantLabel}}',
+ },
+}
+
+export const uk: PluginLanguage = {
+ dateFNSKey: 'uk',
+ translations: ukTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/vi.ts b/packages/plugin-multi-tenant/src/translations/languages/vi.ts
new file mode 100644
index 0000000000..e017d9a05e
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/vi.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const viTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ 'Bạn đang chuẩn bị chuyển quyền sở hữu từ <0>{{fromTenant}}0> sang <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': 'Xác nhận thay đổi {{tenantLabel}}',
+ },
+}
+
+export const vi: PluginLanguage = {
+ dateFNSKey: 'vi',
+ translations: viTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/zh.ts b/packages/plugin-multi-tenant/src/translations/languages/zh.ts
new file mode 100644
index 0000000000..047847258c
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/zh.ts
@@ -0,0 +1,13 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const zhTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body': '您即将将所有权从<0>{{fromTenant}}0>更改为<0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': '确认更改{{tenantLabel}}',
+ },
+}
+
+export const zh: PluginLanguage = {
+ dateFNSKey: 'zh-CN',
+ translations: zhTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/languages/zhTw.ts b/packages/plugin-multi-tenant/src/translations/languages/zhTw.ts
new file mode 100644
index 0000000000..4950b18cb4
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/languages/zhTw.ts
@@ -0,0 +1,14 @@
+import type { PluginDefaultTranslationsObject, PluginLanguage } from '../types.js'
+
+export const zhTwTranslations: PluginDefaultTranslationsObject = {
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body':
+ '您即將將所有權從 <0>{{fromTenant}}0> 轉移至 <0>{{toTenant}}0>',
+ 'confirm-tenant-switch--heading': '確認{{tenantLabel}}更改',
+ },
+}
+
+export const zhTw: PluginLanguage = {
+ dateFNSKey: 'zh-TW',
+ translations: zhTwTranslations,
+}
diff --git a/packages/plugin-multi-tenant/src/translations/types.ts b/packages/plugin-multi-tenant/src/translations/types.ts
new file mode 100644
index 0000000000..d1750a2c80
--- /dev/null
+++ b/packages/plugin-multi-tenant/src/translations/types.ts
@@ -0,0 +1,12 @@
+import type { Language } from '@payloadcms/translations'
+
+import type { enTranslations } from './languages/en.js'
+
+export type PluginLanguage = Language<{
+ 'plugin-multi-tenant': {
+ 'confirm-tenant-switch--body': string
+ 'confirm-tenant-switch--heading': string
+ }
+}>
+
+export type PluginDefaultTranslationsObject = typeof enTranslations
diff --git a/packages/plugin-nested-docs/package.json b/packages/plugin-nested-docs/package.json
index daf8954e84..c7af667b49 100644
--- a/packages/plugin-nested-docs/package.json
+++ b/packages/plugin-nested-docs/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "The official Nested Docs plugin for Payload",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/plugin-redirects/package.json b/packages/plugin-redirects/package.json
index 2aa329ad26..326d2261a7 100644
--- a/packages/plugin-redirects/package.json
+++ b/packages/plugin-redirects/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-redirects",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Redirects plugin for Payload",
"keywords": [
"payload",
diff --git a/packages/plugin-search/package.json b/packages/plugin-search/package.json
index 430bef0eb2..211566e274 100644
--- a/packages/plugin-search/package.json
+++ b/packages/plugin-search/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-search",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Search plugin for Payload",
"keywords": [
"payload",
diff --git a/packages/plugin-sentry/package.json b/packages/plugin-sentry/package.json
index 02bc00753d..432165fb8a 100644
--- a/packages/plugin-sentry/package.json
+++ b/packages/plugin-sentry/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-sentry",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Sentry plugin for Payload",
"keywords": [
"payload",
diff --git a/packages/plugin-seo/package.json b/packages/plugin-seo/package.json
index 769554c198..036119e718 100644
--- a/packages/plugin-seo/package.json
+++ b/packages/plugin-seo/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-seo",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "SEO plugin for Payload",
"keywords": [
"payload",
diff --git a/packages/plugin-stripe/package.json b/packages/plugin-stripe/package.json
index c75b99fd7f..2062949cd4 100644
--- a/packages/plugin-stripe/package.json
+++ b/packages/plugin-stripe/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-stripe",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Stripe plugin for Payload",
"keywords": [
"payload",
diff --git a/packages/plugin-stripe/src/hooks/createNewInStripe.ts b/packages/plugin-stripe/src/hooks/createNewInStripe.ts
index 4aaa12849c..8fdf628f4d 100644
--- a/packages/plugin-stripe/src/hooks/createNewInStripe.ts
+++ b/packages/plugin-stripe/src/hooks/createNewInStripe.ts
@@ -61,7 +61,7 @@ export const createNewInStripe: CollectionBeforeValidateHookWithArgs = async (ar
syncedFields = deepen(syncedFields)
// api version can only be the latest, stripe recommends ts ignoring it
- const stripe = new Stripe(pluginConfig.stripeSecretKey || '', { apiVersion: '2022-08-01' })
+ const stripe = new Stripe(pluginConfig?.stripeSecretKey || '', { apiVersion: '2022-08-01' })
if (operation === 'update') {
if (logs) {
diff --git a/packages/plugin-stripe/src/hooks/deleteFromStripe.ts b/packages/plugin-stripe/src/hooks/deleteFromStripe.ts
index c61b34a71b..8ab021a64b 100644
--- a/packages/plugin-stripe/src/hooks/deleteFromStripe.ts
+++ b/packages/plugin-stripe/src/hooks/deleteFromStripe.ts
@@ -40,7 +40,7 @@ export const deleteFromStripe: CollectionAfterDeleteHookWithArgs = async (args)
if (syncConfig) {
try {
// api version can only be the latest, stripe recommends ts ignoring it
- const stripe = new Stripe(pluginConfig.stripeSecretKey || '', { apiVersion: '2022-08-01' })
+ const stripe = new Stripe(pluginConfig?.stripeSecretKey || '', { apiVersion: '2022-08-01' })
const found = await stripe?.[syncConfig.stripeResourceType]?.retrieve(doc.stripeID)
diff --git a/packages/plugin-stripe/src/hooks/syncExistingWithStripe.ts b/packages/plugin-stripe/src/hooks/syncExistingWithStripe.ts
index 9718270ecf..719f17a9cb 100644
--- a/packages/plugin-stripe/src/hooks/syncExistingWithStripe.ts
+++ b/packages/plugin-stripe/src/hooks/syncExistingWithStripe.ts
@@ -63,7 +63,7 @@ export const syncExistingWithStripe: CollectionBeforeChangeHookWithArgs = async
try {
// api version can only be the latest, stripe recommends ts ignoring it
- const stripe = new Stripe(pluginConfig.stripeSecretKey || '', {
+ const stripe = new Stripe(pluginConfig?.stripeSecretKey || '', {
apiVersion: '2022-08-01',
})
diff --git a/packages/plugin-stripe/src/index.ts b/packages/plugin-stripe/src/index.ts
index 2d1e311f21..3390b2b693 100644
--- a/packages/plugin-stripe/src/index.ts
+++ b/packages/plugin-stripe/src/index.ts
@@ -59,7 +59,7 @@ export const stripePlugin =
})
}
- for (const collection of collections) {
+ for (const collection of collections!) {
const { hooks: existingHooks } = collection
const syncConfig = pluginConfig.sync?.find((sync) => sync.collection === collection.slug)
diff --git a/packages/plugin-stripe/src/routes/rest.ts b/packages/plugin-stripe/src/routes/rest.ts
index da25ebe399..2b57fd7d0d 100644
--- a/packages/plugin-stripe/src/routes/rest.ts
+++ b/packages/plugin-stripe/src/routes/rest.ts
@@ -18,15 +18,7 @@ export const stripeREST = async (args: {
await addDataAndFileToRequest(req)
const requestWithData = req
-
- const {
- data: {
- stripeArgs, // example: ['cus_MGgt3Tuj3D66f2'] or [{ limit: 100 }, { stripeAccount: 'acct_1J9Z4pKZ4Z4Z4Z4Z' }]
- stripeMethod, // example: 'subscriptions.list',
- },
- payload,
- user,
- } = requestWithData
+ const { data, payload, user } = requestWithData
const { stripeSecretKey } = pluginConfig
@@ -37,8 +29,8 @@ export const stripeREST = async (args: {
}
responseJSON = await stripeProxy({
- stripeArgs,
- stripeMethod,
+ stripeArgs: data?.stripeArgs, // example: ['cus_MGgt3Tuj3D66f2'] or [{ limit: 100 }, { stripeAccount: 'acct_1J9Z4pKZ4Z4Z4Z4Z' }]
+ stripeMethod: data?.stripeMethod, // example: 'subscriptions.list',
stripeSecretKey,
})
diff --git a/packages/plugin-stripe/src/routes/webhooks.ts b/packages/plugin-stripe/src/routes/webhooks.ts
index 3373b9da32..dc60841fd0 100644
--- a/packages/plugin-stripe/src/routes/webhooks.ts
+++ b/packages/plugin-stripe/src/routes/webhooks.ts
@@ -26,7 +26,7 @@ export const stripeWebhooks = async (args: {
},
})
- const body = await req.text()
+ const body = await req.text!()
const stripeSignature = req.headers.get('stripe-signature')
if (stripeSignature) {
@@ -41,7 +41,7 @@ export const stripeWebhooks = async (args: {
}
if (event) {
- handleWebhooks({
+ void handleWebhooks({
config,
event,
payload: req.payload,
@@ -52,7 +52,7 @@ export const stripeWebhooks = async (args: {
// Fire external webhook handlers if they exist
if (typeof webhooks === 'function') {
- webhooks({
+ void webhooks({
config,
event,
payload: req.payload,
@@ -65,7 +65,7 @@ export const stripeWebhooks = async (args: {
if (typeof webhooks === 'object') {
const webhookEventHandler = webhooks[event.type]
if (typeof webhookEventHandler === 'function') {
- webhookEventHandler({
+ void webhookEventHandler({
config,
event,
payload: req.payload,
diff --git a/packages/plugin-stripe/tsconfig.json b/packages/plugin-stripe/tsconfig.json
index 1d4d43b8fc..fb21182864 100644
--- a/packages/plugin-stripe/tsconfig.json
+++ b/packages/plugin-stripe/tsconfig.json
@@ -1,8 +1,4 @@
{
"extends": "../../tsconfig.base.json",
- "compilerOptions": {
- /* TODO: remove the following lines */
- "strict": false,
- },
"references": [{ "path": "../payload" }, { "path": "../ui" }, { "path": "../next" }]
}
diff --git a/packages/richtext-lexical/package.json b/packages/richtext-lexical/package.json
index 466ec7f03e..567bd6d3bf 100644
--- a/packages/richtext-lexical/package.json
+++ b/packages/richtext-lexical/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "The officially supported Lexical richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -344,8 +344,7 @@
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
- "prepublishOnly": "pnpm clean && pnpm turbo build",
- "translateNewKeys": "node --no-deprecation --import @swc-node/register/esm-register scripts/translateNewKeys.ts"
+ "prepublishOnly": "pnpm clean && pnpm turbo build"
},
"lint-staged": {
"**/package.json": "sort-package-json",
diff --git a/packages/richtext-lexical/src/features/link/client/index.tsx b/packages/richtext-lexical/src/features/link/client/index.tsx
index c8600e309d..2200c2d20b 100644
--- a/packages/richtext-lexical/src/features/link/client/index.tsx
+++ b/packages/richtext-lexical/src/features/link/client/index.tsx
@@ -64,7 +64,6 @@ const toolbarGroups: ToolbarGroup[] = [
const linkFields: Partial = {
doc: null,
- newTab: false,
}
editor.dispatchCommand(TOGGLE_LINK_WITH_MODAL_COMMAND, {
diff --git a/packages/richtext-lexical/src/features/toolbars/fixed/server/index.ts b/packages/richtext-lexical/src/features/toolbars/fixed/server/index.ts
index c87f33829b..b79b716bf5 100644
--- a/packages/richtext-lexical/src/features/toolbars/fixed/server/index.ts
+++ b/packages/richtext-lexical/src/features/toolbars/fixed/server/index.ts
@@ -1,3 +1,5 @@
+import type { CustomGroups } from '../../types.js'
+
import { createServerFeature } from '../../../../utilities/createServerFeature.js'
export type FixedToolbarFeatureProps = {
@@ -9,6 +11,15 @@ export type FixedToolbarFeatureProps = {
* This means that if the editor has a child-editor, and the child-editor is focused, the toolbar will apply to the child-editor, not the parent editor with this feature added.
*/
applyToFocusedEditor?: boolean
+ /**
+ * Custom configurations for toolbar groups
+ * Key is the group key (e.g. 'format', 'indent', 'align')
+ * Value is a partial ToolbarGroup object that will be merged with the default configuration
+ *
+ * @note Props passed via customGroups must be serializable. Avoid using functions or dynamic components.
+ * ChildComponent, if provided, must be a serializable server component.
+ */
+ customGroups?: CustomGroups
/**
* @default false
*
@@ -26,6 +37,7 @@ export const FixedToolbarFeature = createServerFeature<
const sanitizedProps: FixedToolbarFeatureProps = {
applyToFocusedEditor:
props?.applyToFocusedEditor === undefined ? false : props.applyToFocusedEditor,
+ customGroups: props?.customGroups,
disableIfParentHasFixedToolbar:
props?.disableIfParentHasFixedToolbar === undefined
? false
diff --git a/packages/richtext-lexical/src/features/toolbars/types.ts b/packages/richtext-lexical/src/features/toolbars/types.ts
index 600049ee62..afed211e20 100644
--- a/packages/richtext-lexical/src/features/toolbars/types.ts
+++ b/packages/richtext-lexical/src/features/toolbars/types.ts
@@ -115,3 +115,9 @@ export type ToolbarGroupItem = {
onSelect?: ({ editor, isActive }: { editor: LexicalEditor; isActive: boolean }) => void
order?: number
}
+
+export type CustomGroups = Record<
+ string,
+ | Partial>
+ | Partial>
+>
diff --git a/packages/richtext-lexical/src/field/Field.tsx b/packages/richtext-lexical/src/field/Field.tsx
index 1f5bc28257..c738920761 100644
--- a/packages/richtext-lexical/src/field/Field.tsx
+++ b/packages/richtext-lexical/src/field/Field.tsx
@@ -12,7 +12,7 @@ import {
useField,
} from '@payloadcms/ui'
import { mergeFieldStyles } from '@payloadcms/ui/shared'
-import React, { useCallback, useEffect, useMemo, useState } from 'react'
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import type { SanitizedClientEditorConfig } from '../lexical/config/types.js'
@@ -116,16 +116,30 @@ const RichTextComponent: React.FC<
const pathWithEditDepth = `${path}.${editDepth}`
- const updateFieldValue = (editorState: EditorState) => {
- const newState = editorState.toJSON()
- prevValueRef.current = newState
- setValue(newState)
- }
+ const dispatchFieldUpdateTask = useRef(undefined)
const handleChange = useCallback(
(editorState: EditorState) => {
+ const updateFieldValue = (editorState: EditorState) => {
+ const newState = editorState.toJSON()
+ prevValueRef.current = newState
+ setValue(newState)
+ }
+
if (typeof window.requestIdleCallback === 'function') {
- requestIdleCallback(() => updateFieldValue(editorState))
+ // Cancel earlier scheduled value updates,
+ // so that a CPU-limited event loop isn't flooded with n callbacks for n keystrokes into the rich text field,
+ // but that there's only ever the latest one state update
+ // dispatch task, to be executed with the next idle time,
+ // or the deadline of 500ms.
+ if (typeof window.cancelIdleCallback === 'function' && dispatchFieldUpdateTask.current) {
+ cancelIdleCallback(dispatchFieldUpdateTask.current)
+ }
+ // Schedule the state update to happen the next time the browser has sufficient resources,
+ // or the latest after 500ms.
+ dispatchFieldUpdateTask.current = requestIdleCallback(() => updateFieldValue(editorState), {
+ timeout: 500,
+ })
} else {
updateFieldValue(editorState)
}
diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts
index 4e0fd0083d..31a937a439 100644
--- a/packages/richtext-lexical/src/index.ts
+++ b/packages/richtext-lexical/src/index.ts
@@ -434,6 +434,7 @@ export function lexicalEditor(args?: LexicalEditorProps): LexicalRichTextAdapter
mergeLocaleActions,
operation,
originalDoc,
+ overrideAccess,
parentIsLocalized,
path,
previousValue,
@@ -476,9 +477,11 @@ export function lexicalEditor(args?: LexicalEditorProps): LexicalRichTextAdapter
if (!originalNodeIDMap || !Object.keys(originalNodeIDMap).length || !value) {
return value
}
+
const previousNodeIDMap: {
[key: string]: SerializedLexicalNode
} = {}
+
const originalNodeWithLocalesIDMap: {
[key: string]: SerializedLexicalNode
} = {}
@@ -528,7 +531,6 @@ export function lexicalEditor(args?: LexicalEditorProps): LexicalRichTextAdapter
originalNodeWithLocales: originalNodeWithLocalesIDMap[id],
parentRichTextFieldPath: path,
parentRichTextFieldSchemaPath: schemaPath,
-
previousNode: previousNodeIDMap[id]!,
req,
skipValidation: skipValidation!,
@@ -567,6 +569,7 @@ export function lexicalEditor(args?: LexicalEditorProps): LexicalRichTextAdapter
global,
mergeLocaleActions: mergeLocaleActions!,
operation: operation!,
+ overrideAccess,
parentIndexPath: indexPath.join('-'),
parentIsLocalized: parentIsLocalized || field.localized || false,
parentPath: path.join('.'),
diff --git a/packages/richtext-lexical/src/lexical/config/client/sanitize.ts b/packages/richtext-lexical/src/lexical/config/client/sanitize.ts
index 294683cd3b..41ae552eaf 100644
--- a/packages/richtext-lexical/src/lexical/config/client/sanitize.ts
+++ b/packages/richtext-lexical/src/lexical/config/client/sanitize.ts
@@ -2,6 +2,9 @@
import type { EditorConfig as LexicalEditorConfig } from 'lexical'
+import { deepMerge } from 'payload/shared'
+
+import type { ToolbarGroup } from '../../../features/toolbars/types.js'
import type {
ResolvedClientFeatureMap,
SanitizedClientFeatures,
@@ -31,6 +34,17 @@ export const sanitizeClientFeatures = (
},
}
+ // Allow customization of groups for toolbarFixed
+ let customGroups: Record> = {}
+ features.forEach((feature) => {
+ if (feature.key === 'toolbarFixed' && feature.sanitizedClientFeatureProps?.customGroups) {
+ customGroups = {
+ ...customGroups,
+ ...feature.sanitizedClientFeatureProps.customGroups,
+ }
+ }
+ })
+
if (!features?.size) {
return sanitized
}
@@ -158,6 +172,17 @@ export const sanitizeClientFeatures = (
sanitized.enabledFeatures.push(feature.key)
})
+ // Apply custom group configurations to toolbarFixed groups
+ if (Object.keys(customGroups).length > 0) {
+ sanitized.toolbarFixed.groups = sanitized.toolbarFixed.groups.map((group) => {
+ const customConfig = customGroups[group.key]
+ if (customConfig) {
+ return deepMerge(group, customConfig)
+ }
+ return group
+ })
+ }
+
// Sort sanitized.toolbarInline.groups by order property
sanitized.toolbarInline.groups.sort((a, b) => {
if (a.order && b.order) {
diff --git a/packages/richtext-slate/package.json b/packages/richtext-slate/package.json
index fd720101e9..a0918c4743 100644
--- a/packages/richtext-slate/package.json
+++ b/packages/richtext-slate/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-slate",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "The officially supported Slate richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/storage-azure/package.json b/packages/storage-azure/package.json
index 52ca8d300d..0687d7f541 100644
--- a/packages/storage-azure/package.json
+++ b/packages/storage-azure/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-azure",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Payload storage adapter for Azure Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/storage-gcs/package.json b/packages/storage-gcs/package.json
index 22f2efbcc4..ce7689706b 100644
--- a/packages/storage-gcs/package.json
+++ b/packages/storage-gcs/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-gcs",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Payload storage adapter for Google Cloud Storage",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/storage-s3/package.json b/packages/storage-s3/package.json
index 3c274d09cf..cd4d65c2b9 100644
--- a/packages/storage-s3/package.json
+++ b/packages/storage-s3/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-s3",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Payload storage adapter for Amazon S3",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/storage-s3/src/index.ts b/packages/storage-s3/src/index.ts
index b45b881f49..b9a0b850a6 100644
--- a/packages/storage-s3/src/index.ts
+++ b/packages/storage-s3/src/index.ts
@@ -12,6 +12,8 @@ import * as AWS from '@aws-sdk/client-s3'
import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'
import { initClientUploads } from '@payloadcms/plugin-cloud-storage/utilities'
+import type { SignedDownloadsConfig } from './staticHandler.js'
+
import { getGenerateSignedURLHandler } from './generateSignedURL.js'
import { getGenerateURL } from './generateURL.js'
import { getHandleDelete } from './handleDelete.js'
@@ -24,6 +26,7 @@ export type S3StorageOptions = {
*/
acl?: 'private' | 'public-read'
+
/**
* Bucket name to upload files to.
*
@@ -39,8 +42,15 @@ export type S3StorageOptions = {
/**
* Collection options to apply the S3 adapter to.
*/
- collections: Partial | true>>
-
+ collections: Partial<
+ Record<
+ UploadCollectionSlug,
+ | ({
+ signedDownloads?: SignedDownloadsConfig
+ } & Omit)
+ | true
+ >
+ >
/**
* AWS S3 client configuration. Highly dependent on your AWS setup.
*
@@ -61,6 +71,10 @@ export type S3StorageOptions = {
* Default: true
*/
enabled?: boolean
+ /**
+ * Use pre-signed URLs for files downloading. Can be overriden per-collection.
+ */
+ signedDownloads?: SignedDownloadsConfig
}
type S3StoragePlugin = (storageS3Args: S3StorageOptions) => Plugin
@@ -158,9 +172,27 @@ export const s3Storage: S3StoragePlugin =
function s3StorageInternal(
getStorageClient: () => AWS.S3,
- { acl, bucket, clientUploads, config = {} }: S3StorageOptions,
+ {
+ acl,
+ bucket,
+ clientUploads,
+ collections,
+ config = {},
+ signedDownloads: topLevelSignedDownloads,
+ }: S3StorageOptions,
): Adapter {
return ({ collection, prefix }): GeneratedAdapter => {
+ const collectionStorageConfig = collections[collection.slug]
+
+ let signedDownloads: null | SignedDownloadsConfig =
+ typeof collectionStorageConfig === 'object'
+ ? (collectionStorageConfig.signedDownloads ?? false)
+ : null
+
+ if (signedDownloads === null) {
+ signedDownloads = topLevelSignedDownloads ?? null
+ }
+
return {
name: 's3',
clientUploads,
@@ -173,7 +205,12 @@ function s3StorageInternal(
getStorageClient,
prefix,
}),
- staticHandler: getHandler({ bucket, collection, getStorageClient }),
+ staticHandler: getHandler({
+ bucket,
+ collection,
+ getStorageClient,
+ signedDownloads: signedDownloads ?? false,
+ }),
}
}
}
diff --git a/packages/storage-s3/src/staticHandler.ts b/packages/storage-s3/src/staticHandler.ts
index 0c01319c14..7f328c1072 100644
--- a/packages/storage-s3/src/staticHandler.ts
+++ b/packages/storage-s3/src/staticHandler.ts
@@ -3,13 +3,23 @@ import type { StaticHandler } from '@payloadcms/plugin-cloud-storage/types'
import type { CollectionConfig } from 'payload'
import type { Readable } from 'stream'
+import { GetObjectCommand } from '@aws-sdk/client-s3'
+import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { getFilePrefix } from '@payloadcms/plugin-cloud-storage/utilities'
import path from 'path'
+export type SignedDownloadsConfig =
+ | {
+ /** @default 7200 */
+ expiresIn?: number
+ }
+ | boolean
+
interface Args {
bucket: string
collection: CollectionConfig
getStorageClient: () => AWS.S3
+ signedDownloads?: SignedDownloadsConfig
}
// Type guard for NodeJS.Readable streams
@@ -40,7 +50,12 @@ const streamToBuffer = async (readableStream: any) => {
return Buffer.concat(chunks)
}
-export const getHandler = ({ bucket, collection, getStorageClient }: Args): StaticHandler => {
+export const getHandler = ({
+ bucket,
+ collection,
+ getStorageClient,
+ signedDownloads,
+}: Args): StaticHandler => {
return async (req, { params: { clientUploadContext, filename } }) => {
let object: AWS.GetObjectOutput | undefined = undefined
try {
@@ -48,6 +63,17 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
const key = path.posix.join(prefix, filename)
+ if (signedDownloads && !clientUploadContext) {
+ const command = new GetObjectCommand({ Bucket: bucket, Key: key })
+ const signedUrl = await getSignedUrl(
+ // @ts-expect-error mismatch versions
+ getStorageClient(),
+ command,
+ typeof signedDownloads === 'object' ? signedDownloads : { expiresIn: 7200 },
+ )
+ return Response.redirect(signedUrl)
+ }
+
object = await getStorageClient().getObject({
Bucket: bucket,
Key: key,
diff --git a/packages/storage-uploadthing/package.json b/packages/storage-uploadthing/package.json
index 828a00d6ed..0c2e910360 100644
--- a/packages/storage-uploadthing/package.json
+++ b/packages/storage-uploadthing/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-uploadthing",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Payload storage adapter for uploadthing",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/storage-vercel-blob/package.json b/packages/storage-vercel-blob/package.json
index 3d7ae0b645..7441fe8c35 100644
--- a/packages/storage-vercel-blob/package.json
+++ b/packages/storage-vercel-blob/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-vercel-blob",
- "version": "3.36.1",
+ "version": "3.38.0",
"description": "Payload storage adapter for Vercel Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {
diff --git a/packages/translations/.env.example b/packages/translations/.env.example
deleted file mode 100644
index 22d74139b5..0000000000
--- a/packages/translations/.env.example
+++ /dev/null
@@ -1 +0,0 @@
-OPENAI_KEY=sk-
diff --git a/packages/translations/package.json b/packages/translations/package.json
index 7c4c9d9747..7237f3ba37 100644
--- a/packages/translations/package.json
+++ b/packages/translations/package.json
@@ -1,6 +1,6 @@
{
"name": "@payloadcms/translations",
- "version": "3.36.1",
+ "version": "3.38.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -51,8 +51,7 @@
"clean": "rimraf -g {dist,*.tsbuildinfo}",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
- "prepublishOnly": "pnpm clean && pnpm turbo build",
- "translateNewKeys": "node --no-deprecation --import @swc-node/register/esm-register scripts/translateNewKeys/run.ts"
+ "prepublishOnly": "pnpm clean && pnpm turbo build"
},
"dependencies": {
"date-fns": "4.1.0"
diff --git a/packages/translations/scripts/translateNewKeys/run.ts b/packages/translations/scripts/translateNewKeys/run.ts
deleted file mode 100644
index e903614f3d..0000000000
--- a/packages/translations/scripts/translateNewKeys/run.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import path from 'path'
-import { fileURLToPath } from 'url'
-
-import type { AcceptedLanguages, GenericTranslationsObject } from '../../src/types.js'
-
-import { translations } from '../../src/exports/all.js'
-import { enTranslations } from '../../src/languages/en.js'
-import { translateObject } from './index.js'
-
-const filename = fileURLToPath(import.meta.url)
-const dirname = path.dirname(filename)
-
-const allTranslations: {
- [key in AcceptedLanguages]?: {
- dateFNSKey: string
- translations: GenericTranslationsObject
- }
-} = {}
-
-for (const key of Object.keys(translations)) {
- allTranslations[key] = {
- dateFNSKey: translations[key].dateFNSKey,
- translations: translations[key].translations,
- }
-}
-
-void translateObject({
- allTranslationsObject: allTranslations,
- fromTranslationsObject: enTranslations,
- //languages: ['de'],
- targetFolder: path.resolve(dirname, '../../src/languages'),
-})
diff --git a/packages/translations/src/exports/all.ts b/packages/translations/src/exports/all.ts
index e4e4a8d1f9..6832af1d53 100644
--- a/packages/translations/src/exports/all.ts
+++ b/packages/translations/src/exports/all.ts
@@ -20,6 +20,7 @@ import { it } from '../languages/it.js'
import { ja } from '../languages/ja.js'
import { ko } from '../languages/ko.js'
import { lt } from '../languages/lt.js'
+import { lv } from '../languages/lv.js'
import { my } from '../languages/my.js'
import { nb } from '../languages/nb.js'
import { nl } from '../languages/nl.js'
@@ -60,6 +61,7 @@ export const translations = {
ja,
ko,
lt,
+ lv,
my,
nb,
nl,
diff --git a/packages/translations/src/languages/lv.ts b/packages/translations/src/languages/lv.ts
new file mode 100644
index 0000000000..21e1b56ea1
--- /dev/null
+++ b/packages/translations/src/languages/lv.ts
@@ -0,0 +1,515 @@
+import type { Language } from '../types.js'
+
+export const lvTranslations = {
+ authentication: {
+ account: 'Konts',
+ accountOfCurrentUser: 'Pašreizējā lietotāja konts',
+ accountVerified: 'Konts veiksmīgi verificēts.',
+ alreadyActivated: 'Jau aktivizēts',
+ alreadyLoggedIn: 'Jau pieslēdzies',
+ apiKey: 'API atslēga',
+ authenticated: 'Autentificēts',
+ backToLogin: 'Atpakaļ uz pieslēgšanos',
+ beginCreateFirstUser: 'Lai sāktu, izveidojiet savu pirmo lietotāju.',
+ changePassword: 'Mainīt paroli',
+ checkYourEmailForPasswordReset:
+ 'Ja e-pasta adrese ir saistīta ar kontu, drīz saņemsiet norādījumus paroles atiestatīšanai. Lūdzu, pārbaudiet arī surogātpasta mapi, ja e-pasts nav iesūtnē.',
+ confirmGeneration: 'Apstiprināt ģenerēšanu',
+ confirmPassword: 'Apstiprināt paroli',
+ createFirstUser: 'Izveidot pirmo lietotāju',
+ emailNotValid: 'Norādītais e-pasts nav derīgs',
+ emailOrUsername: 'E-pasts vai lietotājvārds',
+ emailSent: 'E-pasts nosūtīts',
+ emailVerified: 'E-pasts veiksmīgi verificēts.',
+ enableAPIKey: 'Ieslēgt API atslēgu',
+ failedToUnlock: 'Neizdevās atbloķēt',
+ forceUnlock: 'Piespiedu atbloķēšana',
+ forgotPassword: 'Aizmirsi paroli?',
+ forgotPasswordEmailInstructions:
+ 'Lūdzu, ievadiet savu e-pastu zemāk. Saņemsiet ziņojumu ar norādījumiem paroles atiestatīšanai.',
+ forgotPasswordUsernameInstructions:
+ 'Lūdzu, ievadiet savu lietotājvārdu zemāk. Norādījumi paroles atiestatīšanai tiks nosūtīti uz e-pastu, kas saistīts ar jūsu lietotājvārdu.',
+ usernameNotValid: 'Norādītais lietotājvārds nav derīgs',
+
+ forgotPasswordQuestion: 'Aizmirsi paroli?',
+ generate: 'Ģenerēt',
+ generateNewAPIKey: 'Ģenerēt jaunu API atslēgu',
+ generatingNewAPIKeyWillInvalidate:
+ 'Ģenerējot jaunu API atslēgu, <1>iepriekšējā atslēga kļūs nederīga1>. Vai tiešām vēlaties turpināt?',
+ lockUntil: 'Bloķēts līdz',
+ logBackIn: 'Pieslēgties atkārtoti',
+ loggedIn: 'Lai pieslēgtos ar citu lietotāju, vispirms <0>atslēdzieties0>.',
+ loggedInChangePassword:
+ 'Lai mainītu paroli, dodieties uz savu <0>kontu0> un rediģējiet paroli tur.',
+ loggedOutInactivity: 'Jūs esat atslēgts neaktivitātes dēļ.',
+ loggedOutSuccessfully: 'Jūs veiksmīgi atslēdzāties.',
+ loggingOut: 'Notiek atslēgšanās...',
+ login: 'Pieslēgties',
+ loginAttempts: 'Pieslēgšanās mēģinājumi',
+ loginUser: 'Pieslēgt lietotāju',
+ loginWithAnotherUser: 'Lai pieslēgtos ar citu lietotāju, vispirms <0>atslēdzieties0>.',
+ logOut: 'Atslēgties',
+ logout: 'Atslēgties',
+ logoutSuccessful: 'Atslēgšanās veiksmīga.',
+ logoutUser: 'Atslēgt lietotāju',
+ newAccountCreated:
+ 'Jums tikko ir izveidots jauns konts piekļuvei {{serverURL}}. Lūdzu, noklikšķiniet uz šīs saites vai iekopējiet URL pārlūkprogrammā, lai verificētu savu e-pastu: {{verificationURL}} Pēc e-pasta verificēšanas varēsiet veiksmīgi pieslēgties.',
+ newAPIKeyGenerated: 'Jauna API atslēga ģenerēta.',
+ newPassword: 'Jauna parole',
+ passed: 'Autentifikācija veiksmīga',
+ passwordResetSuccessfully: 'Parole veiksmīgi atiestatīta.',
+ resetPassword: 'Atiestatīt paroli',
+ resetPasswordExpiration: 'Paroles atiestatīšanas termiņš',
+ resetPasswordToken: 'Paroles atiestatīšanas tokens',
+ resetYourPassword: 'Atiestatīt savu paroli',
+ stayLoggedIn: 'Palikt pieslēgtam',
+ successfullyRegisteredFirstUser: 'Pirmais lietotājs veiksmīgi reģistrēts.',
+ successfullyUnlocked: 'Veiksmīgi atbloķēts',
+ tokenRefreshSuccessful: 'Tokens veiksmīgi atjaunots.',
+ unableToVerify: 'Neizdevās verificēt',
+ username: 'Lietotājvārds',
+ verified: 'Verificēts',
+ verifiedSuccessfully: 'Veiksmīgi verificēts',
+ verify: 'Verificēt',
+ verifyUser: 'Verificēt lietotāju',
+ verifyYourEmail: 'Verificējiet savu e-pastu',
+ youAreInactive:
+ 'Jūs kādu laiku neesat bijis aktīvs, un drošības nolūkos drīz automātiski tiksiet atslēgts. Vai vēlaties palikt pieslēgts?',
+ youAreReceivingResetPassword:
+ 'Jūs saņemat šo ziņojumu, jo (vai kāds cits) esat pieprasījis paroles atiestatīšanu savam kontam. Lūdzu, noklikšķiniet uz šīs saites vai iekopējiet to pārlūkprogrammā, lai pabeigtu procesu:',
+ youDidNotRequestPassword:
+ 'Ja neesat pieprasījis paroles atiestatīšanu, lūdzu, ignorējiet šo e-pastu, un parole paliks nemainīta.',
+ },
+ error: {
+ accountAlreadyActivated: 'Šis konts jau ir aktivizēts.',
+ autosaving: 'Radās problēma, automātiski saglabājot šo dokumentu.',
+ correctInvalidFields: 'Lūdzu, izlabojiet nederīgos laukus.',
+ deletingFile: 'Radās kļūda, dzēšot failu.',
+ deletingTitle:
+ 'Radās kļūda, dzēšot {{title}}. Lūdzu, pārbaudiet savienojumu un mēģiniet vēlreiz.',
+ emailOrPasswordIncorrect: 'Norādītais e-pasts vai parole nav pareiza.',
+ followingFieldsInvalid_one: 'Šis lauks nav derīgs:',
+ followingFieldsInvalid_other: 'Šie lauki nav derīgi:',
+ incorrectCollection: 'Nepareiza kolekcija',
+ invalidFileType: 'Nederīgs faila tips',
+ invalidFileTypeValue: 'Nederīgs faila tips: {{value}}',
+ invalidRequestArgs: 'Pieprasījumā nodoti nederīgi argumenti: {{args}}',
+ loadingDocument: 'Radās problēma, ielādējot dokumentu ar ID {{id}}.',
+ localesNotSaved_one: 'Šo lokalizāciju nevarēja saglabāt:',
+ localesNotSaved_other: 'Šīs lokalizācijas nevarēja saglabāt:',
+ logoutFailed: 'Neizdevās atslēgties.',
+ missingEmail: 'Trūkst e-pasta.',
+ missingIDOfDocument: 'Trūkst dokumenta ID, ko atjaunināt.',
+ missingIDOfVersion: 'Trūkst versijas ID.',
+ missingRequiredData: 'Trūkst nepieciešamo datu.',
+ noFilesUploaded: 'Nav augšupielādēti faili.',
+ noMatchedField: 'Nav atrasts atbilstošs lauks "{{label}}"',
+ notAllowedToAccessPage: 'Jums nav atļauts piekļūt šai lapai.',
+ notAllowedToPerformAction: 'Jums nav atļauts veikt šo darbību.',
+ notFound: 'Pieprasītais resurss nav atrasts.',
+ noUser: 'Nav lietotāja',
+ previewing: 'Radās problēma, priekšskatot šo dokumentu.',
+ problemUploadingFile: 'Radās problēma, augšupielādējot failu.',
+ tokenInvalidOrExpired: 'Tokens ir nederīgs vai beidzies.',
+ tokenNotProvided: 'Tokens nav norādīts.',
+ unableToDeleteCount: 'Neizdevās izdzēst {{count}} no {{total}} {{label}}.',
+ unableToReindexCollection:
+ 'Radās kļūda, pārindeksējot kolekciju {{collection}}. Operācija pārtraukta.',
+ unableToUpdateCount: 'Neizdevās atjaunināt {{count}} no {{total}} {{label}}.',
+ unauthorized: 'Neautorizēts, jums jāpieslēdzas, lai veiktu šo pieprasījumu.',
+ unauthorizedAdmin: 'Neautorizēts, šim lietotājam nav piekļuves administrācijas panelim.',
+ unknown: 'Radās nezināma kļūda.',
+ unPublishingDocument: 'Radās problēma, atceļot dokumenta publicēšanu.',
+ unspecific: 'Radās kļūda.',
+ unverifiedEmail: 'Lūdzu, verificējiet savu e-pastu pirms pieslēgšanās.',
+ userEmailAlreadyRegistered: 'Lietotājs ar šo e-pastu jau ir reģistrēts.',
+ userLocked: 'Šis lietotājs ir bloķēts pārāk daudzu neveiksmīgu pieslēgšanās mēģinājumu dēļ.',
+ usernameAlreadyRegistered: 'Lietotājs ar šo lietotājvārdu jau ir reģistrēts.',
+ usernameOrPasswordIncorrect: 'Norādītais lietotājvārds vai parole nav pareiza.',
+ valueMustBeUnique: 'Vērtībai jābūt unikālai',
+ verificationTokenInvalid: 'Verifikācijas tokens nav derīgs.',
+ },
+ fields: {
+ addLabel: 'Pievienot {{label}}',
+ addLink: 'Pievienot saiti',
+ addNew: 'Pievienot jaunu',
+ addNewLabel: 'Pievienot jaunu {{label}}',
+ addRelationship: 'Pievienot saistību',
+ addUpload: 'Pievienot augšupielādi',
+ block: 'bloks',
+ blocks: 'bloki',
+ blockType: 'Bloka tips',
+ chooseBetweenCustomTextOrDocument:
+ 'Izvēlieties starp pielāgotu teksta URL vai saiti uz citu dokumentu.',
+ chooseDocumentToLink: 'Izvēlieties dokumentu, uz kuru saistīt',
+ chooseFromExisting: 'Izvēlieties no esošajiem',
+ chooseLabel: 'Izvēlieties {{label}}',
+ collapseAll: 'Sakļaut visus',
+ customURL: 'Pielāgots URL',
+ editLabelData: 'Rediģēt {{label}} datus',
+ editLink: 'Rediģēt saiti',
+ editRelationship: 'Rediģēt saistību',
+ enterURL: 'Ievadiet URL',
+ internalLink: 'Iekšēja saite',
+ itemsAndMore: '{{items}} un vēl {{count}}',
+ labelRelationship: '{{label}} saistība',
+ latitude: 'Platums',
+ linkedTo: 'Saistīts ar <0>{{label}}0>',
+ linkType: 'Saites tips',
+ longitude: 'Garums',
+ newLabel: 'Jauns {{label}}',
+ openInNewTab: 'Atvērt jaunā cilnē',
+ passwordsDoNotMatch: 'Paroles nesakrīt.',
+ relatedDocument: 'Saistītais dokuments',
+ relationTo: 'Saistība ar',
+ removeRelationship: 'Noņemt saistību',
+ removeUpload: 'Noņemt augšupielādi',
+ saveChanges: 'Saglabāt izmaiņas',
+ searchForBlock: 'Meklēt bloku',
+ selectExistingLabel: 'Izvēlēties esošo {{label}}',
+ selectFieldsToEdit: 'Izvēlēties laukus rediģēšanai',
+ showAll: 'Rādīt visus',
+ swapRelationship: 'Mainīt saistību',
+ swapUpload: 'Mainīt augšupielādi',
+ textToDisplay: 'Rādāmais teksts',
+ toggleBlock: 'Pārslēgt bloku',
+ uploadNewLabel: 'Augšupielādēt jaunu {{label}}',
+ },
+ general: {
+ aboutToDelete: 'Jūs grasāties dzēst {{label}} <1>{{title}}1>. Vai esat pārliecināts?',
+ aboutToDeleteCount_many: 'Jūs grasāties dzēst {{count}} {{label}}',
+ aboutToDeleteCount_one: 'Jūs grasāties dzēst {{count}} {{label}}',
+ aboutToDeleteCount_other: 'Jūs grasāties dzēst {{count}} {{label}}',
+ addBelow: 'Pievienot zemāk',
+ addFilter: 'Pievienot filtru',
+ adminTheme: 'Administratora tēma',
+ all: 'Visi',
+ allCollections: 'Visas kolekcijas',
+ and: 'Un',
+ anotherUser: 'Cits lietotājs',
+ anotherUserTakenOver: 'Cits lietotājs ir pārņēmis šī dokumenta rediģēšanu.',
+ applyChanges: 'Pielietot izmaiņas',
+ ascending: 'Augošā secībā',
+ automatic: 'Automātiski',
+ backToDashboard: 'Atpakaļ uz paneli',
+ cancel: 'Atcelt',
+ changesNotSaved: 'Jūsu izmaiņas nav saglabātas. Ja tagad pametīsiet, izmaiņas tiks zaudētas.',
+ clearAll: 'Notīrīt visu',
+ close: 'Aizvērt',
+ collapse: 'Sakļaut',
+ collections: 'Kolekcijas',
+ columns: 'Kolonnas',
+ columnToSort: 'Kolonna kārtošanai',
+ confirm: 'Apstiprināt',
+ confirmCopy: 'Apstiprināt kopēšanu',
+ confirmDeletion: 'Apstiprināt dzēšanu',
+ confirmDuplication: 'Apstiprināt dublēšanu',
+ confirmReindex: 'Pārindeksēt visus {{collections}}?',
+ confirmReindexAll: 'Pārindeksēt visas kolekcijas?',
+ confirmReindexDescription:
+ 'Tas noņems esošos indeksus un pārindeksēs dokumentus kolekcijās {{collections}}.',
+ confirmReindexDescriptionAll:
+ 'Tas noņems esošos indeksus un pārindeksēs dokumentus visās kolekcijās.',
+ copied: 'Nokopēts',
+ copy: 'Kopēt',
+ copying: 'Kopē...',
+ copyWarning:
+ 'Jūs grasāties pārrakstīt {{to}} ar {{from}} priekš {{label}} {{title}}. Vai esat pārliecināts?',
+ create: 'Izveidot',
+ created: 'Izveidots',
+ createdAt: 'Izveidots',
+ createNew: 'Izveidot jaunu',
+ createNewLabel: 'Izveidot jaunu {{label}}',
+ creating: 'Izveido...',
+ creatingNewLabel: 'Izveido jaunu {{label}}',
+ currentlyEditing:
+ 'pašlaik rediģē šo dokumentu. Ja pārņemsiet, viņi tiks bloķēti no turpmākas rediģēšanas un var zaudēt nesaglabātās izmaiņas.',
+ custom: 'Pielāgots',
+ dark: 'Tumšs',
+ dashboard: 'Panelis',
+ delete: 'Dzēst',
+ deletedCountSuccessfully: 'Veiksmīgi izdzēsti {{count}} {{label}}.',
+ deletedSuccessfully: 'Veiksmīgi izdzēsts.',
+ deleting: 'Dzēš...',
+ depth: 'Dziļums',
+ descending: 'Dilstošā secībā',
+ deselectAllRows: 'Atdzēlēt visas rindas',
+ document: 'Dokuments',
+ documentLocked: 'Dokuments bloķēts',
+ documents: 'Dokumenti',
+ duplicate: 'Dublēt',
+ duplicateWithoutSaving: 'Dublēt bez izmaiņu saglabāšanas',
+ edit: 'Rediģēt',
+ editAll: 'Rediģēt visus',
+ editedSince: 'Rediģēts kopš',
+ editing: 'Rediģē',
+ editingLabel_many: 'Rediģē {{count}} {{label}}',
+ editingLabel_one: 'Rediģē {{count}} {{label}}',
+ editingLabel_other: 'Rediģē {{count}} {{label}}',
+ editingTakenOver: 'Rediģēšana pārņemta',
+ editLabel: 'Rediģēt {{label}}',
+ email: 'E-pasts',
+ emailAddress: 'E-pasta adrese',
+ enterAValue: 'Ievadiet vērtību',
+ error: 'Kļūda',
+ errors: 'Kļūdas',
+ fallbackToDefaultLocale: 'Izmantot noklusēto lokalizāciju',
+ false: 'Nepatiesi',
+ filter: 'Filtrs',
+ filters: 'Filtri',
+ filterWhere: 'Filtrēt {{label}} kur',
+ globals: 'Globālie',
+ goBack: 'Doties atpakaļ',
+ isEditing: 'redzē',
+ language: 'Valoda',
+ lastModified: 'Pēdējoreiz mainīts',
+ leaveAnyway: 'Pamest tāpat',
+ leaveWithoutSaving: 'Pamest nesaglabājot',
+ light: 'Gaišs',
+ livePreview: 'Tiešais priekšskatījums',
+ loading: 'Ielādē...',
+ locale: 'Lokalizācija',
+ locales: 'Lokalizācijas',
+ menu: 'Izvēlne',
+ moreOptions: 'Vairāk opciju',
+ moveDown: 'Pārvietot uz leju',
+ moveUp: 'Pārvietot uz augšu',
+ newPassword: 'Jauna parole',
+ next: 'Nākamais',
+ noDateSelected: 'Datums nav izvēlēts',
+ noFiltersSet: 'Nav uzstādīti filtri',
+ noLabel: '