Compare commits

..

2 Commits

Author SHA1 Message Date
Alessio Gravili
cdf42d78be bump eslint-plugin-jest 2025-06-27 12:25:43 -07:00
Alessio Gravili
2fd0a21f4e chore: bump jest to v30 2025-06-27 12:17:32 -07:00
222 changed files with 2005 additions and 4732 deletions

View File

@@ -1,4 +1,4 @@
name: ci
name: build
on:
pull_request:
@@ -187,8 +187,7 @@ jobs:
services:
postgres:
# Custom postgres 17 docker image that supports both pg-vector and postgis: https://github.com/payloadcms/postgis-vector
image: ${{ (startsWith(matrix.database, 'postgres') ) && 'ghcr.io/payloadcms/postgis-vector:latest' || '' }}
image: ${{ (startsWith(matrix.database, 'postgres') ) && 'postgis/postgis:16-3.4' || '' }}
env:
# must specify password for PG Docker container image, see: https://registry.hub.docker.com/_/postgres?tab=description&page=1&name=10
POSTGRES_USER: ${{ env.POSTGRES_USER }}
@@ -316,7 +315,6 @@ jobs:
- plugin-cloud-storage
- plugin-form-builder
- plugin-import-export
- plugin-multi-tenant
- plugin-nested-docs
- plugin-seo
- sort
@@ -453,7 +451,6 @@ jobs:
- plugin-cloud-storage
- plugin-form-builder
- plugin-import-export
- plugin-multi-tenant
- plugin-nested-docs
- plugin-seo
- sort
@@ -529,9 +526,7 @@ jobs:
# Build listed templates with packed local packages and then runs their int and e2e tests
build-and-test-templates:
runs-on: ubuntu-24.04
needs: [changes, build]
if: ${{ needs.changes.outputs.needs_build == 'true' }}
name: build-template-${{ matrix.template }}-${{ matrix.database }}
needs: build
strategy:
fail-fast: false
matrix:
@@ -561,6 +556,8 @@ jobs:
# - template: with-vercel-website
# database: postgres
name: ${{ matrix.template }}-${{ matrix.database }}
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres

View File

@@ -32,18 +32,18 @@ The Admin Panel serves as the entire HTTP layer for Payload, providing a full CR
Once you [install Payload](../getting-started/installation), the following files and directories will be created in your app:
```plaintext
app
├─ (payload)
├── admin
├─── [[...segments]]
app/
├─ (payload)/
├── admin/
├─── [[...segments]]/
├──── page.tsx
├──── not-found.tsx
├── api
├─── [...slug]
├── api/
├─── [...slug]/
├──── route.ts
├── graphql
├── graphql/
├──── route.ts
├── graphql-playground
├── graphql-playground/
├──── route.ts
├── custom.scss
├── layout.tsx
@@ -84,30 +84,29 @@ import { buildConfig } from 'payload'
const config = buildConfig({
// ...
// highlight-start
admin: {
// highlight-line
// ...
},
// highlight-end
})
```
The following options are available:
| Option | Description |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
| `autoLogin` | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
| `components` | Component overrides that affect the entirety of the Admin Panel. [More details](../custom-components/overview). |
| `custom` | Any custom properties you wish to pass to the Admin Panel. |
| `dateFormat` | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
| `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| `meta` | Base metadata to use for the Admin Panel. [More details](./metadata). |
| `routes` | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
| `suppressHydrationWarning` | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
| `theme` | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
| `timezones` | Configure the timezone settings for the admin panel. [More details](#timezones) |
| `user` | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
| Option | Description |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **`avatar`** | Set account profile picture. Options: `gravatar`, `default` or a custom React component. |
| **`autoLogin`** | Used to automate log-in for dev and demonstration convenience. [More details](../authentication/overview). |
| **`components`** | Component overrides that affect the entirety of the Admin Panel. [More details](../custom-components/overview). |
| **`custom`** | Any custom properties you wish to pass to the Admin Panel. |
| **`dateFormat`** | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. |
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). |
| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
| **`suppressHydrationWarning`** | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `<html>` tag. Defaults to `false`. |
| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. |
| **`timezones`** | Configure the timezone settings for the admin panel. [More details](#timezones) |
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
<Banner type="success">
**Reminder:** These are the _root-level_ options for the Admin Panel. You can
@@ -187,12 +186,6 @@ The following options are available:
| `graphQL` | `/graphql` | The [GraphQL API](../graphql/overview) base path. |
| `graphQLPlayground` | `/graphql-playground` | The GraphQL Playground. |
<Banner type="warning">
**Important:** Changing Root-level Routes also requires a change to [Project
Structure](#project-structure) to match the new route. [More
details](#customizing-root-level-routes).
</Banner>
<Banner type="success">
**Tip:** You can easily add _new_ routes to the Admin Panel through [Custom
Endpoints](../rest-api/overview#custom-endpoints) and [Custom
@@ -203,29 +196,13 @@ The following options are available:
You can change the Root-level Routes as needed, such as to mount the Admin Panel at the root of your application.
This change, however, also requires a change to your [Project Structure](#project-structure) to match the new route.
For example, if you set `routes.admin` to `/`:
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
routes: {
admin: '/', // highlight-line
},
})
```
Then you would need to completely remove the `admin` directory from the project structure:
Changing Root-level Routes also requires a change to [Project Structure](#project-structure) to match the new route. For example, if you set `routes.admin` to `/`, you would need to completely remove the `admin` directory from the project structure:
```plaintext
app
├─ (payload)
├── [[...segments]]
app/
├─ (payload)/
├── [[...segments]]/
├──── ...
├── layout.tsx
```
<Banner type="warning">

View File

@@ -51,7 +51,7 @@ For more granular control, pass a configuration object instead. Payload exposes
| Property | Description |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Component` \* | Pass in the component path that should be rendered when a user navigates to this route. |
| `path` \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. Must begin with a forward slash (`/`). |
| `path` \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. |
| `exact` | Boolean. When true, will only match if the path matches the `usePathname()` exactly. |
| `strict` | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. |
| `sensitive` | When true, will match if the path is case sensitive. |

View File

@@ -315,8 +315,7 @@ import type { Field } from 'payload'
export const MyField: Field = {
type: 'text',
name: 'myField',
validate: (value, { req: { t } }) =>
Boolean(value) || t('validation:required'), // highlight-line
validate: (value, {req: { t }}) => Boolean(value) || t('validation:required'), // highlight-line
}
```

View File

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

View File

@@ -2,7 +2,7 @@
import { cn } from '@/utilities/ui'
import useClickableCard from '@/utilities/useClickableCard'
import Link from 'next/link'
import { useLocale } from 'next-intl'
import { useLocale } from 'next-intl';
import React, { Fragment } from 'react'
import type { Post } from '@/payload-types'
@@ -17,7 +17,7 @@ export const Card: React.FC<{
showCategories?: boolean
title?: string
}> = (props) => {
const locale = useLocale()
const locale = useLocale();
const { card, link } = useClickableCard({})
const { className, doc, relationTo, showCategories, title: titleFromProps } = props

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.45.0",
"version": "3.44.0",
"private": true,
"type": "module",
"workspaces": [
@@ -125,7 +125,7 @@
"tsconfig.base.json": "node scripts/reset-tsconfig.js"
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@jest/globals": "30.0.3",
"@libsql/client": "0.14.0",
"@next/bundle-analyzer": "15.3.2",
"@payloadcms/db-postgres": "workspace:*",
@@ -139,7 +139,7 @@
"@swc/cli": "0.7.7",
"@swc/jest": "0.2.38",
"@types/fs-extra": "^11.0.2",
"@types/jest": "29.5.12",
"@types/jest": "30.0.0",
"@types/minimist": "1.2.5",
"@types/node": "22.15.30",
"@types/react": "19.1.0",
@@ -151,22 +151,22 @@
"create-payload-app": "workspace:*",
"cross-env": "7.0.3",
"dotenv": "16.4.7",
"drizzle-kit": "0.31.4",
"drizzle-orm": "0.44.2",
"drizzle-kit": "0.31.0",
"drizzle-orm": "0.43.1",
"escape-html": "^1.0.3",
"execa": "5.1.1",
"form-data": "3.0.1",
"fs-extra": "10.1.0",
"globby": "11.1.0",
"husky": "9.0.11",
"jest": "29.7.0",
"jest": "30.0.3",
"lint-staged": "15.2.7",
"minimist": "1.2.8",
"mongodb-memory-server": "10.1.4",
"next": "15.3.2",
"open": "^10.1.0",
"p-limit": "^5.0.0",
"pg": "8.16.3",
"pg": "8.11.3",
"playwright": "1.50.0",
"playwright-core": "1.50.0",
"prettier": "3.5.3",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.45.0",
"version": "3.44.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -76,7 +76,7 @@
"devDependencies": {
"@types/esprima": "^4.0.6",
"@types/fs-extra": "^9.0.12",
"@types/jest": "29.5.12",
"@types/jest": "30.0.0",
"@types/node": "22.15.30"
},
"engines": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -53,7 +53,6 @@ type Args = {
fields: FlattenedField[]
joins: BuildQueryJoinAliases
locale?: string
parentAliasTable?: PgTableWithColumns<any> | SQLiteTableWithColumns<any>
parentIsLocalized: boolean
pathSegments: string[]
rootTableName?: string
@@ -84,7 +83,6 @@ export const getTableColumnFromPath = ({
fields,
joins,
locale: incomingLocale,
parentAliasTable,
parentIsLocalized,
pathSegments: incomingSegments,
rootTableName: incomingRootTableName,
@@ -164,7 +162,6 @@ export const getTableColumnFromPath = ({
table: adapter.tables[newTableName],
})
}
return getTableColumnFromPath({
adapter,
collectionPath,
@@ -173,7 +170,6 @@ export const getTableColumnFromPath = ({
fields: field.flattenedFields,
joins,
locale,
parentAliasTable: aliasTable,
parentIsLocalized: parentIsLocalized || field.localized,
pathSegments: pathSegments.slice(1),
rootTableName,
@@ -552,10 +548,7 @@ export const getTableColumnFromPath = ({
// Join in the relationships table
if (locale && isFieldLocalized && adapter.payload.config.localization) {
const conditions = [
eq(
(parentAliasTable || aliasTable || adapter.tables[rootTableName]).id,
aliasRelationshipTable.parent,
),
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
]
@@ -573,10 +566,7 @@ export const getTableColumnFromPath = ({
// Join in the relationships table
addJoinTable({
condition: and(
eq(
(parentAliasTable || aliasTable || adapter.tables[rootTableName]).id,
aliasRelationshipTable.parent,
),
eq((aliasTable || adapter.tables[rootTableName]).id, aliasRelationshipTable.parent),
like(aliasRelationshipTable.path, `${constraintPath}${field.name}`),
),
joins,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-resend",
"version": "3.45.0",
"version": "3.44.0",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {
@@ -42,8 +42,8 @@
"test": "jest"
},
"devDependencies": {
"@types/jest": "29.5.12",
"jest": "^29.7.0",
"@types/jest": "30.0.0",
"jest": "30.0.3",
"payload": "workspace:*"
},
"peerDependencies": {

View File

@@ -32,7 +32,7 @@
"eslint": "9.22.0",
"eslint-config-prettier": "10.1.1",
"eslint-plugin-import-x": "4.6.1",
"eslint-plugin-jest": "28.11.0",
"eslint-plugin-jest": "29.0.1",
"eslint-plugin-jest-dom": "5.5.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-perfectionist": "3.9.1",

View File

@@ -31,7 +31,7 @@
"eslint": "9.22.0",
"eslint-config-prettier": "10.1.1",
"eslint-plugin-import-x": "4.6.1",
"eslint-plugin-jest": "28.11.0",
"eslint-plugin-jest": "29.0.1",
"eslint-plugin-jest-dom": "5.5.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-perfectionist": "3.9.1",

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.45.0",
"version": "3.44.0",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.45.0",
"version": "3.44.0",
"description": "The official Vue SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "3.45.0",
"version": "3.44.0",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import type { TypedUser } from 'payload'
import type { User } from 'payload'
import { formatAdminURL } from 'payload/shared'
import * as qs from 'qs-esm'
@@ -7,7 +7,7 @@ type Args = {
config
route: string
searchParams: { [key: string]: string | string[] }
user?: TypedUser
user?: User
}
export const handleAuthRedirect = ({ config, route, searchParams, user }: Args): string => {

View File

@@ -6,7 +6,7 @@ import type {
PayloadRequest,
SanitizedConfig,
SanitizedPermissions,
TypedUser,
User,
} from 'payload'
import { initI18n } from '@payloadcms/translations'
@@ -37,7 +37,7 @@ type PartialResult = {
languageCode: AcceptedLanguages
payload: Payload
responseHeaders: Headers
user: null | TypedUser
user: null | User
}
// Create cache instances for different parts of our application

View File

@@ -1,5 +1,5 @@
'use client'
import type { TypedUser } from 'payload'
import type { User } from 'payload'
import { Button, ConfirmationModal, toast, useModal, useTranslation } from '@payloadcms/ui'
import * as qs from 'qs-esm'
@@ -9,7 +9,7 @@ const confirmResetModalSlug = 'confirm-reset-modal'
export const ResetPreferences: React.FC<{
readonly apiRoute: string
readonly user?: TypedUser
readonly user?: User
}> = ({ apiRoute, user }) => {
const { openModal } = useModal()
const { t } = useTranslation()

View File

@@ -1,5 +1,5 @@
import type { I18n } from '@payloadcms/translations'
import type { BasePayload, Config, LanguageOptions, TypedUser } from 'payload'
import type { BasePayload, Config, LanguageOptions, User } from 'payload'
import { FieldLabel } from '@payloadcms/ui'
import React from 'react'
@@ -17,7 +17,7 @@ export const Settings: React.FC<{
readonly languageOptions: LanguageOptions
readonly payload: BasePayload
readonly theme: Config['admin']['theme']
readonly user?: TypedUser
readonly user?: User
}> = (props) => {
const { className, i18n, languageOptions, payload, theme, user } = props

View File

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

View File

@@ -4,8 +4,8 @@ import {
type PayloadRequest,
type SelectType,
type Sort,
type TypedUser,
type TypeWithVersion,
type User,
type Where,
} from 'payload'
@@ -28,7 +28,7 @@ export const fetchVersion = async <TVersionData extends object = object>({
overrideAccess?: boolean
req: PayloadRequest
select?: SelectType
user?: TypedUser
user?: User
}): Promise<null | TypeWithVersion<TVersionData>> => {
try {
if (collectionSlug) {
@@ -88,7 +88,7 @@ export const fetchVersions = async <TVersionData extends object = object>({
req: PayloadRequest
select?: SelectType
sort?: Sort
user?: TypedUser
user?: User
where?: Where
}): Promise<null | PaginatedDocs<TypeWithVersion<TVersionData>>> => {
const where: Where = { and: [...(whereFromArgs ? [whereFromArgs] : [])] }
@@ -160,7 +160,7 @@ export const fetchLatestVersion = async <TVersionData extends object = object>({
req: PayloadRequest
select?: SelectType
status: 'draft' | 'published'
user?: TypedUser
user?: User
where?: Where
}): Promise<null | TypeWithVersion<TVersionData>> => {
const and: Where[] = [

View File

@@ -4,7 +4,7 @@ import type {
SanitizedCollectionConfig,
SanitizedConfig,
SanitizedGlobalConfig,
TypedUser,
User,
} from 'payload'
export type DefaultVersionsViewProps = {
@@ -18,6 +18,6 @@ export type DefaultVersionsViewProps = {
i18n: I18n
id: number | string
limit: number
user: TypedUser
user: User
versionsData: PaginatedDocs<Document>
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/payload-cloud",
"version": "3.45.0",
"version": "3.44.0",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {
@@ -51,7 +51,7 @@
"nodemailer": "6.9.16"
},
"devDependencies": {
"@types/jest": "29.5.12",
"@types/jest": "30.0.0",
"@types/nodemailer": "6.4.17",
"payload": "workspace:*"
},

View File

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

View File

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

View File

@@ -1,9 +1,8 @@
import type { I18nClient } from '@payloadcms/translations'
import type { MarkOptional } from 'ts-essentials'
import type { SanitizedFieldPermissions } from '../../auth/types.js'
import type { SanitizedFieldPermissions, User } from '../../auth/types.js'
import type { ClientBlock, ClientField, Field } from '../../fields/config/types.js'
import type { TypedUser } from '../../index.js'
import type { DocumentPreferences } from '../../preferences/types.js'
import type { Operation, Payload, PayloadRequest } from '../../types/index.js'
import type {
@@ -91,7 +90,7 @@ export type ServerComponentProps = {
preferences: DocumentPreferences
req: PayloadRequest
siblingData: Data
user: TypedUser
user: User
value?: unknown
}

View File

@@ -23,10 +23,7 @@ export type AdminViewConfig = {
/** Whether the path should be matched exactly or as a prefix */
exact?: boolean
meta?: MetaConfig
/**
* Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. Must begin with a forward slash (`/`).
*/
path?: `/${string}`
path?: string
sensitive?: boolean
strict?: boolean
}

View File

@@ -4,9 +4,11 @@ import type {
AuthOperationsFromCollectionSlug,
Collection,
DataFromCollectionSlug,
SanitizedCollectionConfig,
} from '../../collections/config/types.js'
import type { CollectionSlug, TypedUser } from '../../index.js'
import type { CollectionSlug } from '../../index.js'
import type { PayloadRequest, Where } from '../../types/index.js'
import type { User } from '../types.js'
import { buildAfterOperation } from '../../collections/operations/utils.js'
import {
@@ -31,7 +33,7 @@ import { resetLoginAttempts } from '../strategies/local/resetLoginAttempts.js'
export type Result = {
exp?: number
token?: string
user?: TypedUser
user?: User
}
export type Arguments<TSlug extends CollectionSlug> = {
@@ -44,12 +46,14 @@ export type Arguments<TSlug extends CollectionSlug> = {
}
type CheckLoginPermissionArgs = {
collection: SanitizedCollectionConfig
loggingInWithUsername?: boolean
req: PayloadRequest
user: any
}
export const checkLoginPermission = ({
collection,
loggingInWithUsername,
req,
user,
@@ -58,6 +62,10 @@ export const checkLoginPermission = ({
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)
}
@@ -205,6 +213,7 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
})
checkLoginPermission({
collection: collectionConfig,
loggingInWithUsername: Boolean(canLoginWithUsername && sanitizedUsername),
req,
user,
@@ -232,10 +241,6 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
throw new AuthenticationError(req.t)
}
if (collectionConfig.auth.verify && user._verified === false) {
throw new UnverifiedEmail({ t: req.t })
}
const fieldsToSignArgs: Parameters<typeof getFieldsToSign>[0] = {
collectionConfig,
email: sanitizedEmail!,

View File

@@ -1,9 +1,8 @@
import { decodeJwt } from 'jose'
import type { Collection } from '../../collections/config/types.js'
import type { TypedUser } from '../../index.js'
import type { PayloadRequest } from '../../types/index.js'
import type { ClientUser } from '../types.js'
import type { ClientUser, User } from '../types.js'
export type MeOperationResult = {
collection?: string
@@ -43,7 +42,7 @@ export const meOperation = async (args: Arguments): Promise<MeOperationResult> =
overrideAccess: false,
req,
showHiddenFields: false,
})) as TypedUser
})) as User
if (user) {
user.collection = collection.config.slug

View File

@@ -3,9 +3,8 @@ import { URL } from 'url'
import type { Collection } from '../collections/config/types.js'
import type { SanitizedConfig } from '../config/types.js'
import type { InitializedEmailAdapter } from '../email/types.js'
import type { TypedUser } from '../index.js'
import type { PayloadRequest } from '../types/index.js'
import type { VerifyConfig } from './types.js'
import type { User, VerifyConfig } from './types.js'
type Args = {
collection: Collection
@@ -14,7 +13,7 @@ type Args = {
email: InitializedEmailAdapter
req: PayloadRequest
token: string
user: TypedUser
user: User
}
export async function sendVerificationEmail(args: Args): Promise<void> {

View File

@@ -1,9 +1,8 @@
import crypto from 'crypto'
import type { SanitizedCollectionConfig } from '../../collections/config/types.js'
import type { TypedUser } from '../../index.js'
import type { Where } from '../../types/index.js'
import type { AuthStrategyFunction } from '../index.js'
import type { AuthStrategyFunction, User } from '../index.js'
export const APIKeyAuthentication =
(collectionConfig: SanitizedCollectionConfig): AuthStrategyFunction =>
@@ -50,7 +49,7 @@ export const APIKeyAuthentication =
user!._strategy = 'api-key'
return {
user: user as TypedUser,
user: user as User,
}
}
} catch (ignore) {

View File

@@ -1,6 +1,6 @@
import type { DeepRequired } from 'ts-essentials'
import type { CollectionSlug, GlobalSlug, Payload, TypedUser } from '../index.js'
import type { CollectionSlug, GlobalSlug, Payload } from '../index.js'
import type { PayloadRequest, Where } from '../types/index.js'
/**
@@ -122,10 +122,7 @@ type BaseUser = {
username?: string
}
/**
* @deprecated Use `TypedUser` instead. This will be removed in 4.0.
*/
export type UntypedUser = {
export type User = {
[key: string]: any
} & BaseUser
@@ -182,7 +179,7 @@ export type AuthStrategyResult = {
| ({
_strategy?: string
collection?: string
} & TypedUser)
} & User)
| null
}

View File

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

View File

@@ -375,7 +375,7 @@ type BaseDocumentViewConfig = {
export type CustomDocumentViewConfig =
| ({
Component: DocumentViewComponent
path: `/${string}`
path: string
} & BaseDocumentViewConfig)
| ({
Component?: DocumentViewComponent
@@ -1029,17 +1029,6 @@ export type Config = {
*/
graphQL?: {
disable?: boolean
/**
* Disable introspection queries in production.
*
* @default true
*/
disableIntrospectionInProduction?: boolean
/**
* Disable the GraphQL Playground in production.
*
* @default true
*/
disablePlaygroundInProduction?: boolean
maxComplexity?: number
/**
@@ -1173,35 +1162,18 @@ export type Config = {
filterConstraints?: SelectField['filterOptions']
labels?: CollectionConfig['labels']
}
/**
* Control the routing structure that Payload binds itself to.
* @link https://payloadcms.com/docs/admin/overview#root-level-routes
*/
/** Control the routing structure that Payload binds itself to. */
routes?: {
/**
* The route for the admin panel.
* @example "/my-admin" or "/"
/** The route for the admin panel.
* @example "/my-admin"
* @default "/admin"
* @link https://payloadcms.com/docs/admin/overview#root-level-routes
*/
admin?: string
/**
* The base route for all REST API endpoints.
* @default "/api"
* @link https://payloadcms.com/docs/admin/overview#root-level-routes
*/
/** @default "/api" */
api?: string
/**
* The base route for all GraphQL endpoints.
* @default "/graphql"
* @link https://payloadcms.com/docs/admin/overview#root-level-routes
*/
/** @default "/graphql" */
graphQL?: string
/**
* The route for the GraphQL Playground.
* @default "/graphql-playground"
* @link https://payloadcms.com/docs/admin/overview#root-level-routes
*/
/** @default "/graphql-playground" */
graphQLPlayground?: string
}
/** Secure string that Payload will use for any encryption workflows */

View File

@@ -15,7 +15,7 @@ import type { AuthArgs } from './auth/operations/auth.js'
import type { Result as ForgotPasswordResult } from './auth/operations/forgotPassword.js'
import type { Result as LoginResult } from './auth/operations/login.js'
import type { Result as ResetPasswordResult } from './auth/operations/resetPassword.js'
import type { AuthStrategy, UntypedUser } from './auth/types.js'
import type { AuthStrategy, User } from './auth/types.js'
import type {
BulkOperationResult,
Collection,
@@ -216,7 +216,7 @@ export interface GeneratedTypes {
}
}
localeUntyped: null | string
userUntyped: UntypedUser
userUntyped: User
}
// Helper type to resolve the correct type using conditional types
@@ -299,10 +299,6 @@ type ResolveLocaleType<T> = 'locale' extends keyof T ? T['locale'] : T['localeUn
type ResolveUserType<T> = 'user' extends keyof T ? T['user'] : T['userUntyped']
export type TypedLocale = ResolveLocaleType<GeneratedTypes>
/**
* @todo rename to `User` in 4.0
*/
export type TypedUser = ResolveUserType<GeneratedTypes>
// @ts-expect-error
@@ -1103,7 +1099,7 @@ export type {
SanitizedFieldPermissions,
SanitizedGlobalPermission,
SanitizedPermissions,
UntypedUser as User,
User,
VerifyConfig,
} from './auth/types.js'
export { generateImportMap } from './bin/generateImportMap/index.js'

View File

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

View File

@@ -1,4 +1,5 @@
import type { Payload, RequestContext, TypedLocale, TypedUser } from '../index.js'
import type { User } from '../auth/types.js'
import type { Payload, RequestContext, TypedLocale } from '../index.js'
import type { PayloadRequest } from '../types/index.js'
import { getDataLoader } from '../collections/dataloader.js'
@@ -90,7 +91,7 @@ export type CreateLocalReqOptions = {
locale?: string
req?: Partial<PayloadRequest>
urlSuffix?: string
user?: TypedUser
user?: User
}
type CreateLocalReq = (options: CreateLocalReqOptions, payload: Payload) => Promise<PayloadRequest>

View File

@@ -1,5 +1,5 @@
import type { User } from '../../auth/types.js'
import type { Field } from '../../fields/config/types.js'
import type { TypedUser } from '../../index.js'
import type { TaskConfig } from '../../queues/config/types/taskTypes.js'
import type { SchedulePublishTaskInput } from './types.js'
@@ -21,14 +21,14 @@ export const getSchedulePublishTask = ({
const userID = input.user
let user: null | TypedUser = null
let user: null | User = null
if (userID) {
user = (await req.payload.findByID({
id: userID,
collection: adminUserSlug,
depth: 0,
})) as TypedUser
})) as User
user.collection = adminUserSlug
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import type { Config, TaskConfig, TypedUser } from 'payload'
import type { Config, TaskConfig, User } from 'payload'
import type { CreateExportArgs, Export } from './createExport.js'
@@ -29,13 +29,13 @@ export const getCreateCollectionExportTask = (
return {
slug: 'createCollectionExport',
handler: async ({ input, req }: CreateExportArgs) => {
let user: TypedUser | undefined
let user: undefined | User
if (input.userCollection && input.user) {
user = (await req.payload.findByID({
id: input.user,
collection: input.userCollection,
})) as TypedUser
})) as User
}
if (!user) {

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
import type { RelationshipFieldClientProps } from 'payload'
import { RelationshipField, useField, useFormModified } from '@payloadcms/ui'
import { RelationshipField, useField } from '@payloadcms/ui'
import React from 'react'
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
@@ -18,14 +18,7 @@ type Props = {
export const TenantField = (args: Props) => {
const { debug, unique } = args
const { setValue, value } = useField<number | string>()
const modified = useFormModified()
const {
options,
selectedTenantID,
setEntityType: setEntityType,
setModified,
setTenant,
} = useTenantSelection()
const { options, selectedTenantID, setPreventRefreshOnChange, setTenant } = useTenantSelection()
const hasSetValueRef = React.useRef(false)
@@ -42,25 +35,18 @@ export const TenantField = (args: Props) => {
hasSetValueRef.current = true
} else if (!value || value !== selectedTenantID) {
// Update the field on the document value when the tenant is changed
setValue(selectedTenantID, !value || value === selectedTenantID)
setValue(selectedTenantID)
}
}, [value, selectedTenantID, setTenant, setValue, options, unique])
React.useEffect(() => {
setEntityType(unique ? 'global' : 'document')
return () => {
setEntityType(undefined)
if (!unique) {
setPreventRefreshOnChange(true)
}
}, [unique, setEntityType])
React.useEffect(() => {
// sync form modified state with the tenant selection provider context
setModified(modified)
return () => {
setModified(false)
setPreventRefreshOnChange(false)
}
}, [modified, setModified])
}, [unique, setPreventRefreshOnChange])
if (debug) {
return (

View File

@@ -20,12 +20,11 @@ import type {
import { useTenantSelection } from '../../providers/TenantSelectionProvider/index.client.js'
import './index.scss'
const confirmSwitchTenantSlug = 'confirm-switch-tenant'
const confirmLeaveWithoutSavingSlug = 'confirm-leave-without-saving'
const confirmSwitchTenantSlug = 'confirmSwitchTenant'
export const TenantSelector = ({ label, viewType }: { label: string; viewType?: ViewTypes }) => {
const { entityType, modified, options, selectedTenantID, setTenant } = useTenantSelection()
const { closeModal, openModal } = useModal()
const { options, preventRefreshOnChange, selectedTenantID, setTenant } = useTenantSelection()
const { openModal } = useModal()
const { i18n, t } = useTranslation<
PluginMultiTenantTranslations,
PluginMultiTenantTranslationKeys
@@ -61,27 +60,15 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
const onChange = React.useCallback(
(option: ReactSelectOption | ReactSelectOption[]) => {
if (option && 'value' in option && option.value === selectedTenantID) {
// If the selected option is the same as the current tenant, do nothing
if (!preventRefreshOnChange) {
switchTenant(option)
return
}
if (entityType !== 'document') {
if (entityType === 'global' && modified) {
// If the entityType is 'global' and there are unsaved changes, prompt for confirmation
setTenantSelection(option)
openModal(confirmLeaveWithoutSavingSlug)
} else {
// If the entityType is not 'document', switch tenant without confirmation
switchTenant(option)
}
} else {
// non-unique documents should always prompt for confirmation
setTenantSelection(option)
openModal(confirmSwitchTenantSlug)
}
},
[selectedTenantID, entityType, modified, switchTenant, openModal],
[openModal, preventRefreshOnChange, switchTenant],
)
if (options.length <= 1) {
@@ -118,28 +105,22 @@ export const TenantSelector = ({ label, viewType }: { label: string; viewType?:
}}
/>
}
heading={t('plugin-multi-tenant:confirm-tenant-switch--heading', {
tenantLabel: getTranslation(label, i18n),
})}
heading={
<Translation
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
i18nKey="plugin-multi-tenant:confirm-tenant-switch--heading"
t={t}
variables={{
tenantLabel: getTranslation(label, i18n),
}}
/>
}
modalSlug={confirmSwitchTenantSlug}
onConfirm={() => {
switchTenant(tenantSelection)
}}
/>
<ConfirmationModal
body={t('general:changesNotSaved')}
cancelLabel={t('general:stayOnThisPage')}
confirmLabel={t('general:leaveAnyway')}
heading={t('general:leaveWithoutSaving')}
modalSlug={confirmLeaveWithoutSavingSlug}
onCancel={() => {
closeModal(confirmLeaveWithoutSavingSlug)
}}
onConfirm={() => {
switchTenant(tenantSelection)
}}
/>
</div>
)
}

View File

@@ -8,8 +8,6 @@ import {
useDocumentTitle,
useEffectEvent,
useFormFields,
useFormSubmitted,
useOperation,
} from '@payloadcms/ui'
import React from 'react'
@@ -17,9 +15,8 @@ import { useTenantSelection } from '../../providers/TenantSelectionProvider/inde
export const WatchTenantCollection = () => {
const { id, collectionSlug } = useDocumentInfo()
const operation = useOperation()
const submitted = useFormSubmitted()
const { title } = useDocumentTitle()
const addedNewTenant = React.useRef(false)
const { getEntityConfig } = useConfig()
const [useAsTitleName] = React.useState(
@@ -27,7 +24,7 @@ export const WatchTenantCollection = () => {
)
const titleField = useFormFields(([fields]) => (useAsTitleName ? fields[useAsTitleName] : {}))
const { syncTenants, updateTenants } = useTenantSelection()
const { options, updateTenants } = useTenantSelection()
const syncTenantTitle = useEffectEvent(() => {
if (id) {
@@ -35,19 +32,27 @@ export const WatchTenantCollection = () => {
}
})
React.useEffect(() => {
if (!id || !title || addedNewTenant.current) {
return
}
// Track tenant creation and add it to the tenant selector
const exists = options.some((opt) => opt.value === id)
if (!exists) {
addedNewTenant.current = true
updateTenants({ id, label: title })
}
// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id])
React.useEffect(() => {
// only update the tenant selector when the document saves
// → aka when initial value changes
if (id && titleField?.initialValue) {
void syncTenantTitle()
syncTenantTitle()
}
}, [id, titleField?.initialValue, syncTenants])
React.useEffect(() => {
if (operation === 'create' && submitted) {
void syncTenants()
}
}, [operation, submitted, syncTenants, id])
}, [id, titleField?.initialValue])
return null
}

View File

@@ -2,39 +2,26 @@
import type { OptionObject } from 'payload'
import { toast, useAuth, useConfig } from '@payloadcms/ui'
import { useAuth } from '@payloadcms/ui'
import { useRouter } from 'next/navigation.js'
import React, { createContext } from 'react'
type ContextType = {
/**
* What is the context of the selector? It is either 'document' | 'global' | undefined.
*
* - 'document' means you are viewing a document in the context of a tenant
* - 'global' means you are viewing a "global" (globals are collection documents but prevent you from viewing the list view) document in the context of a tenant
* - undefined means you are not viewing a document at all
*/
entityType?: 'document' | 'global'
/**
* Hoists the forms modified state
*/
modified?: boolean
/**
* Array of options to select from
*/
options: OptionObject[]
preventRefreshOnChange: boolean
/**
* The currently selected tenant ID
*/
selectedTenantID: number | string | undefined
/**
* Sets the entityType when a document is loaded and sets it to undefined when the document unmounts.
* Prevents a refresh when the tenant is changed
*
* If not switching tenants while viewing a "global", set to true
*/
setEntityType: React.Dispatch<React.SetStateAction<'document' | 'global' | undefined>>
/**
* Sets the modified state
*/
setModified: React.Dispatch<React.SetStateAction<boolean>>
setPreventRefreshOnChange: React.Dispatch<React.SetStateAction<boolean>>
/**
* Sets the selected tenant ID
*
@@ -42,10 +29,6 @@ type ContextType = {
* @param args.refresh - Whether to refresh the page after changing the tenant
*/
setTenant: (args: { id: number | string | undefined; refresh?: boolean }) => void
/**
* Used to sync tenants displayed in the tenant selector when updates are made to the tenants collection.
*/
syncTenants: () => Promise<void>
/**
*
*/
@@ -53,13 +36,11 @@ type ContextType = {
}
const Context = createContext<ContextType>({
entityType: undefined,
options: [],
preventRefreshOnChange: false,
selectedTenantID: undefined,
setEntityType: () => undefined,
setModified: () => undefined,
setPreventRefreshOnChange: () => null,
setTenant: () => null,
syncTenants: () => Promise.resolve(),
updateTenants: () => null,
})
@@ -68,23 +49,17 @@ export const TenantSelectionProviderClient = ({
initialValue,
tenantCookie,
tenantOptions: tenantOptionsFromProps,
tenantsCollectionSlug,
useAsTitle,
}: {
children: React.ReactNode
initialValue?: number | string
tenantCookie?: string
tenantOptions: OptionObject[]
tenantsCollectionSlug: string
useAsTitle: string
}) => {
const [selectedTenantID, setSelectedTenantID] = React.useState<number | string | undefined>(
initialValue,
)
const [modified, setModified] = React.useState<boolean>(false)
const [entityType, setEntityType] = React.useState<'document' | 'global' | undefined>(undefined)
const [preventRefreshOnChange, setPreventRefreshOnChange] = React.useState(false)
const { user } = useAuth()
const { config } = useConfig()
const userID = React.useMemo(() => user?.id, [user?.id])
const [tenantOptions, setTenantOptions] = React.useState<OptionObject[]>(
() => tenantOptionsFromProps,
@@ -110,105 +85,73 @@ export const TenantSelectionProviderClient = ({
({ id, refresh }) => {
if (id === undefined) {
if (tenantOptions.length > 1) {
// users with multiple tenants can clear the tenant selection
setSelectedTenantID(undefined)
deleteCookie()
} else {
// if there is only one tenant, force the selection of that tenant
setSelectedTenantID(tenantOptions[0]?.value)
setCookie(String(tenantOptions[0]?.value))
}
} else if (!tenantOptions.find((option) => option.value === id)) {
// if the tenant is not valid, set the first tenant as selected
if (tenantOptions?.[0]?.value) {
setTenant({ id: tenantOptions[0].value, refresh: true })
} else {
setTenant({ id: undefined, refresh: true })
}
} else {
// if the tenant is in the options, set it as selected
setSelectedTenantID(id)
setCookie(String(id))
}
if (entityType !== 'document' && refresh) {
if (!preventRefreshOnChange && refresh) {
router.refresh()
}
},
[deleteCookie, entityType, router, setCookie, tenantOptions],
[deleteCookie, preventRefreshOnChange, router, setCookie, setSelectedTenantID, tenantOptions],
)
const syncTenants = React.useCallback(async () => {
try {
const req = await fetch(
`${config.serverURL}${config.routes.api}/${tenantsCollectionSlug}?select[${useAsTitle}]=true&limit=0&depth=0`,
{
credentials: 'include',
method: 'GET',
},
)
const result = await req.json()
if (result.docs) {
setTenantOptions(
result.docs.map((doc: Record<string, number | string>) => ({
label: doc[useAsTitle],
value: doc.id,
})),
)
}
} catch (e) {
toast.error(`Error fetching tenants`)
}
}, [config.serverURL, config.routes.api, tenantsCollectionSlug, useAsTitle])
const updateTenants = React.useCallback<ContextType['updateTenants']>(
({ id, label }) => {
setTenantOptions((prev) => {
return prev.map((currentTenant) => {
if (id === currentTenant.value) {
return {
label,
value: id,
}
const updateTenants = React.useCallback<ContextType['updateTenants']>(({ id, label }) => {
setTenantOptions((prev) => {
const stringID = String(id)
let exists = false
const updated = prev.map((currentTenant) => {
if (stringID === String(currentTenant.value)) {
exists = true
return {
label,
value: stringID,
}
return currentTenant
})
}
return currentTenant
})
void syncTenants()
},
[syncTenants],
)
if (!exists) {
updated.push({ label, value: stringID })
}
// Sort alphabetically by label (or value as fallback)
return updated.sort((a, b) => {
const aKey = typeof a.label === 'string' ? a.label : String(a.value)
const bKey = typeof b.label === 'string' ? b.label : String(b.value)
return aKey.localeCompare(bKey)
})
})
}, [])
React.useEffect(() => {
if (selectedTenantID && !tenantOptions.find((option) => option.value === selectedTenantID)) {
if (tenantOptions?.[0]?.value) {
setTenant({ id: tenantOptions[0].value, refresh: true })
} else {
setTenant({ id: undefined, refresh: true })
}
}
}, [tenantCookie, setTenant, selectedTenantID, tenantOptions, initialValue, setCookie])
React.useEffect(() => {
if (userID && !tenantCookie) {
if (tenantOptionsFromProps.length === 1) {
// Users with no cookie set and only 1 tenant should set that tenant automatically
setTenant({ id: tenantOptionsFromProps[0]?.value, refresh: true })
setTenantOptions(tenantOptionsFromProps)
} else if (
(!tenantOptions || tenantOptions.length === 0) &&
tenantOptionsFromProps.length > 0
) {
// If there are no tenant options, set them from the props
setTenantOptions(tenantOptionsFromProps)
}
} else if (userID && tenantCookie) {
if ((!tenantOptions || tenantOptions.length === 0) && tenantOptionsFromProps.length > 0) {
// If there are no tenant options, set them from the props
setTenantOptions(tenantOptionsFromProps)
// User is logged in, but does not have a tenant cookie, set it
setSelectedTenantID(initialValue)
setTenantOptions(tenantOptionsFromProps)
if (initialValue) {
setCookie(String(initialValue))
} else {
deleteCookie()
}
}
}, [
initialValue,
selectedTenantID,
tenantCookie,
userID,
setTenant,
tenantOptionsFromProps,
tenantOptions,
])
}, [userID, tenantCookie, initialValue, setCookie, deleteCookie, router, tenantOptionsFromProps])
React.useEffect(() => {
if (!userID && tenantCookie) {
@@ -228,14 +171,11 @@ export const TenantSelectionProviderClient = ({
>
<Context
value={{
entityType,
modified,
options: tenantOptions,
preventRefreshOnChange,
selectedTenantID,
setEntityType,
setModified,
setPreventRefreshOnChange,
setTenant,
syncTenants,
updateTenants,
}}
>

View File

@@ -1,4 +1,4 @@
import type { OptionObject, Payload, TypedUser } from 'payload'
import type { OptionObject, Payload, User } from 'payload'
import { cookies as getCookies } from 'next/headers.js'
@@ -10,7 +10,7 @@ type Args = {
payload: Payload
tenantsCollectionSlug: string
useAsTitle: string
user: TypedUser
user: User
}
export const TenantSelectionProvider = async ({
@@ -65,8 +65,6 @@ export const TenantSelectionProvider = async ({
initialValue={initialValue}
tenantCookie={tenantCookie}
tenantOptions={tenantOptions}
tenantsCollectionSlug={tenantsCollectionSlug}
useAsTitle={useAsTitle}
>
{children}
</TenantSelectionProviderClient>

View File

@@ -1,11 +1,11 @@
import type { PaginatedDocs, Payload, TypedUser } from 'payload'
import type { PaginatedDocs, Payload, User } from 'payload'
type Args = {
limit: number
payload: Payload
tenantsCollectionSlug: string
useAsTitle: string
user?: TypedUser
user?: User
}
export const findTenantOptions = async ({
limit,

View File

@@ -1,5 +1,5 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import type { ArrayField, CollectionSlug, Field, RelationshipField, TypedUser } from 'payload'
import type { ArrayField, CollectionSlug, Field, RelationshipField, User } from 'payload'
export type MultiTenantPluginConfig<ConfigTypes = unknown> = {
/**
@@ -138,7 +138,7 @@ export type MultiTenantPluginConfig<ConfigTypes = unknown> = {
* Useful for super-admin type users
*/
userHasAccessToAllTenants?: (
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : TypedUser,
user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : User,
) => boolean
/**
* Opt out of adding access constraints to the tenants collection
@@ -165,4 +165,4 @@ export type UserWithTenantsField = {
tenant: number | string | Tenant
}[]
| null
} & TypedUser
} & User

View File

@@ -1,4 +1,4 @@
import type { Payload, TypedUser, ViewTypes } from 'payload'
import type { Payload, User, ViewTypes } from 'payload'
import { formatAdminURL } from 'payload/shared'
@@ -15,7 +15,7 @@ type Args = {
tenantFieldName: string
tenantsCollectionSlug: string
useAsTitle: string
user?: TypedUser
user?: User
view: ViewTypes
}
export async function getGlobalViewRedirect({

View File

@@ -4,7 +4,7 @@ import type {
AccessResult,
AllOperations,
CollectionConfig,
TypedUser,
User,
Where,
} from 'payload'
@@ -53,7 +53,7 @@ export const withTenantAccess =
args.req.user &&
args.req.user.collection === adminUsersSlug &&
!userHasAccessToAllTenants(
args.req.user as ConfigType extends { user: unknown } ? ConfigType['user'] : TypedUser,
args.req.user as ConfigType extends { user: unknown } ? ConfigType['user'] : User,
)
) {
const tenantConstraint = getTenantAccess({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import type {
SanitizedConfig,
TextField,
TextFieldSingleValidation,
TypedUser,
User,
} from 'payload'
import type { LinkFields } from '../nodes/types.js'
@@ -120,7 +120,7 @@ export const getBaseFields = (
? ({ relationTo, user }) => {
const hidden = config.collections.find(({ slug }) => slug === relationTo)?.admin
.hidden
if (typeof hidden === 'function' && hidden({ user } as { user: TypedUser })) {
if (typeof hidden === 'function' && hidden({ user } as { user: User })) {
return false
}
return true

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import type { Field, SanitizedConfig, TypedUser } from 'payload'
import type { Field, SanitizedConfig, User } from 'payload'
export const getBaseFields = (config: SanitizedConfig): Field[] => [
{
@@ -47,7 +47,7 @@ export const getBaseFields = (config: SanitizedConfig): Field[] => [
type: 'relationship',
filterOptions: ({ relationTo, user }) => {
const hidden = config.collections.find(({ slug }) => slug === relationTo).admin.hidden
if (typeof hidden === 'function' && hidden({ user } as { user: TypedUser })) {
if (typeof hidden === 'function' && hidden({ user } as { user: User })) {
return false
}
},

View File

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

View File

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

View File

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

View File

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

View File

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

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