Compare commits

..

32 Commits

Author SHA1 Message Date
Elliot DeNolf
78c8bb81a1 chore(release): v3.0.0-beta.95 [skip ci] 2024-08-28 14:49:15 -04:00
Elliot DeNolf
419b274bb1 chore: move ui and translations into deps from peerDeps (#7929)
Move `ui` and `translations` from peerDeps into deps for a few packages.
Users should not have to install these directly unless they are making
customizations.
2024-08-28 14:47:02 -04:00
Alessio Gravili
ef818fd5c8 fix(ui): admin.dependencies components not added to client config (#7928) 2024-08-28 13:56:52 -04:00
Germán Jabloñski
0aaf3af1ea fix(richtext-lexical): enabledCollections and disabledCollections props in RelationshipFeature (#7926)
fixes https://github.com/payloadcms/payload/issues/7379

The enabledCollections and disabledCollections properties of the
RelationshipFeature were not being sent to the client and therefore did
not have the expected effect.

Now those 2 properties are sent to the client via the
`clientFeatureProps` property.
2024-08-28 13:45:10 -04:00
James Mikrut
18b0806b5b fix(db-postgres, db-sqlite): hasMany text, number, poly relationship, blocks, arrays within localized fields (#7900)
## Description

In Postgres, localized blocks or arrays that contain other array / block
/ relationship fields were not properly storing locales in the database.

Now they are! Need to check a few things yet:

- Ensure test coverage is sufficient
- Test localized array, with non-localized array inside of it
- Test localized block with relationship field within it
- Ensure `_rels` table gets the `locale` column added if a single
non-localized relationship exists within a localized array / block

Fixes step 6 as identified in #7805
2024-08-28 17:43:12 +00:00
Jacob Fletcher
3d9051ad34 test: extracts reorderColumns e2e util for reuse (#7923) 2024-08-28 12:20:47 -04:00
Jarrod Flesch
e4ef47b938 chore(examples): multi tenant single domain fixes (#7922) 2024-08-28 11:34:22 -04:00
Alessio Gravili
c7e7dc71d3 fix(richtext-lexical): ensure html converter text is escaped (#7919) 2024-08-28 14:31:06 +00:00
Jarrod Flesch
375671c162 fix: active nav item set incorrectly in child routes (#7918) 2024-08-28 10:00:13 -04:00
Elliot DeNolf
23b495b145 build: update turborepo npm scripts (#7899)
Updating all turborepo npm scripts for this rather inconvenient breaking
change: https://github.com/vercel/turborepo/pull/8137
2024-08-27 22:04:05 -04:00
Paul
27d743e2a8 fix: depth not being respected by upload has many (#7897) 2024-08-28 00:50:29 +00:00
Elliot DeNolf
8c9ff3d54b revert(scripts): publish script progress prefix 2024-08-27 19:53:15 -04:00
Elliot DeNolf
5c447252e7 chore(release): v3.0.0-beta.94 [skip ci] 2024-08-27 19:47:37 -04:00
Jarrod Flesch
a76be81368 fix: upload has many field updates (#7894)
## Description

Implements fixes and style changes for the upload field component.

Fixes https://github.com/payloadcms/payload/issues/7819

![CleanShot 2024-08-27 at 16 22
33](https://github.com/user-attachments/assets/fa27251c-20b8-45ad-9109-55dee2e19e2f)

![CleanShot 2024-08-27 at 16 22
49](https://github.com/user-attachments/assets/de2d24f9-b2f5-4b72-abbe-24a6c56a4c21)


- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

## Type of change

- [ ] Chore (non-breaking change which does not add functionality)
- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Change to the
[templates](https://github.com/payloadcms/payload/tree/main/templates)
directory (does not affect core functionality)
- [ ] Change to the
[examples](https://github.com/payloadcms/payload/tree/main/examples)
directory (does not affect core functionality)
- [ ] This change requires a documentation update

## Checklist:

- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] Existing test suite passes locally with my changes
- [ ] I have made corresponding changes to the documentation

---------

Co-authored-by: Paul Popus <paul@nouance.io>
2024-08-27 19:07:18 -04:00
Paul
5d97d57e70 feat(templates): add new slug component to the website (#7895)
https://github.com/user-attachments/assets/1ba125d3-9c65-4bab-98df-fb80c70eeb71
2024-08-27 22:26:56 +00:00
Alessio Gravili
de7ff1f8c6 fix(richtext-lexical): html converters can populate relationships infinitely (#7890)
Fixes https://github.com/payloadcms/payload/issues/7743
2024-08-27 15:24:50 -04:00
James Mikrut
3d714d3e72 fix: locale selector + autosave race condition (#7891)
## Description

Fixes a race condition where you could switch locales and have autosave
trigger with old locale data.

By adding the `key` to the `Document` component, we will ensure that the
entire `Document` will be un-mounted and re-mounted between locale
switches.
2024-08-27 14:56:28 -04:00
Elliot DeNolf
2bbb02b9c0 chore(scripts): add package count to publish script [skip ci] 2024-08-27 14:41:47 -04:00
Elliot DeNolf
0533e7f5db chore(release): v3.0.0-beta.93 [skip ci] 2024-08-27 14:16:30 -04:00
Paul
23c5ef428d fix: bugs in versions UI with perPage, pagination and diffs not showing for tabs. publish button no longer double saves when using keyboard (#7889)
Fixes:
- issue with publish button double saving on keyboard command
- versions diffs not showing if fields are tabs
https://github.com/payloadcms/payload/issues/7860
- navigation on versions not working for perPage and pagination
2024-08-27 17:45:30 +00:00
Alessio Gravili
f046a04510 fix(richtext-lexical): dependency checker suggesting incorrect version (#7888)
Fixes this:
https://discord.com/channels/967097582721572934/1278031296970625190/1278031652089757818
2024-08-27 17:17:19 +00:00
Elliot DeNolf
4cda7d2363 chore(release): v3.0.0-beta.92 [skip ci] 2024-08-27 09:44:02 -04:00
Elliot DeNolf
ea48cfbfe9 feat: implement info command (#7882)
Implements `info` command similar to Next.js.

`pnpm payload info` will output info in this format:

```
Binaries:
  Node: 18.20.2
  npm: 10.5.0
  Yarn: 1.22.19
  pnpm: 9.7.0
Relevant Packages:
  payload: 3.0.0-beta.91
  next: 15.0.0-canary.104
  @payloadcms/db-mongodb: 3.0.0-beta.91
  @payloadcms/db-postgres: 3.0.0-beta.91
  @payloadcms/email-nodemailer: 3.0.0-beta.91
  @payloadcms/graphql: 3.0.0-beta.91
  @payloadcms/next/utilities: 3.0.0-beta.91
  @payloadcms/plugin-cloud: 3.0.0-beta.91
  @payloadcms/richtext-lexical: 3.0.0-beta.91
  @payloadcms/richtext-slate: 3.0.0-beta.91
  @payloadcms/translations: 3.0.0-beta.91
  @payloadcms/ui/shared: 3.0.0-beta.91
  react: 19.0.0-rc-06d0b89e-20240801
  react-dom: 19.0.0-rc-06d0b89e-20240801
Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 23.6.0: Mon Jul 29 21:13:04 PDT 2024; root:xnu-10063.141.2~1/RELEASE_ARM64_T6020
  Available memory (MB): 32768
  Available CPU cores: 12
 ```
2024-08-27 01:41:39 +00:00
Paul
1aeb912762 fix(templates): website live preview and code block (#7881) 2024-08-26 23:42:45 +00:00
Paul
ce2cb35d71 fix(plugin-seo): remove dependency on import from payload/next package (#7879) 2024-08-26 22:40:38 +00:00
Paul
d3ec68ac2f fix: error when closing the live preview popup window (#7878) 2024-08-26 22:33:56 +00:00
Paul
05bf52aac3 fix(templates): website bug fixes for slug generation and form builder and adds support for strictNullChecks: true (#7877) 2024-08-26 22:10:09 +00:00
Jacob Fletcher
fed7f2fa5b fix: sanitizes modifyResponseHeaders from client config (#7876) 2024-08-26 21:50:29 +00:00
James Mikrut
686b0865b2 fix: exports richtext-slate useSlatePlugin (#7875)
## Description

exports `useSlatePlugin` for `richtext-slate`
2024-08-26 17:21:25 -04:00
Alessio Gravili
dfb4c8eb4c feat: add nextjs and react version checks to dependency checker (#7868) 2024-08-26 17:19:14 -04:00
Alessio Gravili
ad7a387e19 feat(richtext-lexical): more lenient url validation, URL-encode invalid urls on save (#7870)
Fixes https://github.com/payloadcms/payload/issues/7477

This simplifies validation to the point where it only errors on spaces.
Actual validation is then used in beforeChange, which then automatically
url encodes the input if it doesn't pass
2024-08-26 15:33:29 -04:00
Elliot DeNolf
d05be016ce ci(scripts): emoji release notes 2024-08-23 16:25:50 -04:00
206 changed files with 15203 additions and 2730 deletions

View File

@@ -196,6 +196,48 @@ import { MyFieldComponent } from 'my-external-package/client'
which is a valid way to access MyFieldComponent that can be resolved by the consuming project.
### Custom Components from unknown locations
By default, any component paths from known locations are added to the import map. However, if you need to add any components from unknown locations to the import map, you can do so by adding them to the `admin.dependencies` array in your Payload Config. This is mostly only relevant for plugin authors and not for regular Payload users.
Example:
```ts
export default {
// ...
admin: {
// ...
dependencies: {
myTestComponent: { // myTestComponent is the key - can be anything
path: '/components/TestComponent.js#TestComponent',
type: 'component',
clientProps: {
test: 'hello',
},
},
},
}
}
```
This way, `TestComponent` is added to the import map, no matter if it's referenced in a known location or not. On the client, you can then use the component like this:
```tsx
'use client'
import { RenderComponent, useConfig } from '@payloadcms/ui'
import React from 'react'
export const CustomView = () => {
const { config } = useConfig()
return (
<div>
<RenderComponent mappedComponent={config.admin.dependencies?.myTestComponent} />
</div>
)
}
```
## Root Components
Root Components are those that effect the [Admin Panel](./overview) generally, such as the logo or the main nav.

View File

@@ -5,6 +5,8 @@ import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = {
params: {
segments: string[]
@@ -17,6 +19,7 @@ type Args = {
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
const NotFound = ({ params, searchParams }: Args) =>
NotFoundPage({ config, importMap, params, searchParams })
export default NotFound

View File

@@ -5,6 +5,8 @@ import config from '@payload-config'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'
import { importMap } from '../importMap.js'
type Args = {
params: {
segments: string[]
@@ -17,6 +19,7 @@ type Args = {
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
generatePageMetadata({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
const Page = ({ params, searchParams }: Args) =>
RootPage({ config, importMap, params, searchParams })
export default Page

View File

@@ -0,0 +1,7 @@
import { TenantFieldComponent as TenantFieldComponent_0 } from '@/fields/TenantField/components/Field'
import { TenantSelectorRSC as TenantSelectorRSC_1 } from '@/components/TenantSelector'
export const importMap = {
'@/fields/TenantField/components/Field#TenantFieldComponent': TenantFieldComponent_0,
'@/components/TenantSelector#TenantSelectorRSC': TenantSelectorRSC_1,
}

View File

@@ -1,18 +1,21 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
import configPromise from "@payload-config";
import "@payloadcms/next/css";
import { RootLayout } from "@payloadcms/next/layouts";
import configPromise from '@payload-config'
import '@payloadcms/next/css'
import { RootLayout } from '@payloadcms/next/layouts'
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import React from "react";
import React from 'react'
import "./custom.scss";
import { importMap } from './admin/importMap.js'
import './custom.scss'
type Args = {
children: React.ReactNode;
};
children: React.ReactNode
}
const Layout = ({ children }: Args) => (
<RootLayout config={configPromise}>{children}</RootLayout>
);
<RootLayout config={configPromise} importMap={importMap}>
{children}
</RootLayout>
)
export default Layout;
export default Layout

View File

@@ -1,6 +1,6 @@
import type { Access } from 'payload'
import type { User } from '../../../../payload-types'
import type { User } from '../../../payload-types'
import { isSuperAdmin } from '../../../access/isSuperAdmin'
import { getTenantAdminTenantAccessIDs } from '../../../utilities/getTenantAccessIDs'

View File

@@ -1,6 +1,6 @@
import type { CollectionConfig } from 'payload'
import type { User } from '../../../payload-types'
import type { User } from '../../payload-types'
import { getTenantAdminTenantAccessIDs } from '../../utilities/getTenantAccessIDs'
import { createAccess } from './access/create'
@@ -37,32 +37,6 @@ const Users: CollectionConfig = {
{
name: 'tenant',
type: 'relationship',
filterOptions: ({ user }) => {
if (!user) {
// Would like to query where exists true on id
// but that is not working
return {
id: {
like: '',
},
}
}
if (user?.roles?.includes('super-admin')) {
// Would like to query where exists true on id
// but that is not working
return {
id: {
like: '',
},
}
}
const adminTenantAccessIDs = getTenantAdminTenantAccessIDs(user as User)
return {
id: {
in: adminTenantAccessIDs,
},
}
},
index: true,
relationTo: 'tenants',
required: true,

View File

@@ -2,17 +2,18 @@
import type { Option } from '@payloadcms/ui/elements/ReactSelect'
import type { OptionObject } from 'payload'
import { getTenantAdminTenantAccessIDs } from '@/utilities/getTenantAccessIDs'
import { SelectInput, useAuth } from '@payloadcms/ui'
import * as qs from 'qs-esm'
import React from 'react'
import type { Tenant, User } from '../../../payload-types.js'
import type { Tenant, User } from '../../payload-types'
import './index.scss'
export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) => {
const { user } = useAuth<User>()
const [options, setOptions] = React.useState<OptionObject[]>([])
const [value, setValue] = React.useState<string | undefined>(initialCookie)
const isSuperAdmin = user?.roles?.includes('super-admin')
const tenantIDs =
@@ -28,18 +29,6 @@ export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) =>
document.cookie = name + '=' + (value || '') + expires + '; path=/'
}
React.useEffect(() => {
const fetchTenants = async () => {
const res = await fetch(`/api/tenants?depth=0&limit=100&sort=name`, {
credentials: 'include',
}).then((res) => res.json())
setOptions(res.docs.map((doc: Tenant) => ({ label: doc.name, value: doc.id })))
}
void fetchTenants()
}, [])
const handleChange = React.useCallback((option: Option | Option[]) => {
if (!option) {
setCookie('payload-tenant', undefined)
@@ -50,7 +39,44 @@ export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) =>
}
}, [])
if (isSuperAdmin || tenantIDs.length > 1) {
React.useEffect(() => {
const fetchTenants = async () => {
const adminOfTenants = getTenantAdminTenantAccessIDs(user ?? null)
const queryString = qs.stringify(
{
depth: 0,
limit: 100,
sort: 'name',
where: {
id: {
in: adminOfTenants,
},
},
},
{
addQueryPrefix: true,
},
)
const res = await fetch(`/api/tenants${queryString}`, {
credentials: 'include',
}).then((res) => res.json())
const optionsToSet = res.docs.map((doc: Tenant) => ({ label: doc.name, value: doc.id }))
if (optionsToSet.length === 1) {
setCookie('payload-tenant', optionsToSet[0].value)
}
setOptions(optionsToSet)
}
if (user) {
void fetchTenants()
}
}, [user])
if ((isSuperAdmin || tenantIDs.length > 1) && options.length > 1) {
return (
<div className="tenant-selector">
<SelectInput
@@ -59,7 +85,7 @@ export const TenantSelector = ({ initialCookie }: { initialCookie?: string }) =>
onChange={handleChange}
options={options}
path="setTenant"
value={value}
value={options.find((opt) => opt.value === initialCookie)?.value}
/>
</div>
)

View File

@@ -0,0 +1,34 @@
'use client'
import { RelationshipField, useField } from '@payloadcms/ui'
import React from 'react'
type Props = {
initialValue?: string
path: string
readOnly: boolean
}
export function TenantFieldComponentClient({ initialValue, path, readOnly }: Props) {
const { formInitializing, setValue } = useField({ path })
const hasSetInitialValue = React.useRef(false)
React.useEffect(() => {
if (!hasSetInitialValue.current && !formInitializing && initialValue) {
setValue(initialValue)
hasSetInitialValue.current = true
}
}, [initialValue, setValue, formInitializing])
return (
<RelationshipField
field={{
name: path,
type: 'relationship',
_path: path,
label: 'Tenant',
relationTo: 'tenants',
required: true,
}}
readOnly={readOnly}
/>
)
}

View File

@@ -1,26 +1,32 @@
'use client'
import { RelationshipField, useAuth, useFieldProps } from '@payloadcms/ui'
import type { Payload } from 'payload'
import { cookies as getCookies, headers as getHeaders } from 'next/headers'
import React from 'react'
import type { User } from '../../../../payload-types.js'
import { TenantFieldComponentClient } from './Field.client'
export const TenantFieldComponent = () => {
const { user } = useAuth<User>()
const { path, readOnly } = useFieldProps()
export const TenantFieldComponent: React.FC<{
path: string
payload: Payload
readOnly: boolean
}> = async (args) => {
const cookies = getCookies()
const headers = getHeaders()
const { user } = await args.payload.auth({ headers })
if (user) {
if ((user.tenants && user.tenants.length > 1) || user?.roles?.includes('super-admin')) {
return (
<RelationshipField
label="Tenant"
name={path}
path={path}
readOnly={readOnly}
relationTo="tenants"
required
/>
)
}
if (
user &&
((Array.isArray(user.tenants) && user.tenants.length > 1) ||
user?.roles?.includes('super-admin'))
) {
return (
<TenantFieldComponentClient
initialValue={cookies.get('payload-tenant')?.value || undefined}
path={args.path}
readOnly={args.readOnly}
/>
)
}
return null
}

View File

@@ -2,7 +2,6 @@ import type { Field } from 'payload'
import { isSuperAdmin } from '../../access/isSuperAdmin'
import { tenantFieldUpdate } from './access/update'
import { TenantFieldComponent } from './components/Field'
import { autofillTenant } from './hooks/autofillTenant'
export const tenantField: Field = {
@@ -17,7 +16,7 @@ export const tenantField: Field = {
},
admin: {
components: {
Field: TenantFieldComponent,
Field: '@/fields/TenantField/components/Field#TenantFieldComponent',
},
position: 'sidebar',
},

View File

@@ -17,6 +17,9 @@ export interface Config {
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
db: {
defaultIDType: string;
};
globals: {};
locale: null;
user: User & {
@@ -26,15 +29,20 @@ export interface Config {
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
password: string;
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema

View File

@@ -7,7 +7,6 @@ import { fileURLToPath } from 'url'
import { Pages } from './collections/Pages'
import { Tenants } from './collections/Tenants'
import Users from './collections/Users'
import { TenantSelectorRSC } from './components/TenantSelector'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
@@ -15,7 +14,7 @@ const dirname = path.dirname(filename)
export default buildConfig({
admin: {
components: {
afterNavLinks: [TenantSelectorRSC],
afterNavLinks: ['@/components/TenantSelector#TenantSelectorRSC'],
},
user: 'users',
},

View File

@@ -1,4 +1,4 @@
import type { User } from '../../payload-types'
import type { User } from '../payload-types'
export const getTenantAccessIDs = (user: User | null): string[] => {
if (!user) return []

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@ export default withBundleAnalyzer(
env: {
PAYLOAD_CORE_DEV: 'true',
ROOT_DIR: path.resolve(dirname),
PAYLOAD_DISABLE_DEPENDENCY_CHECKER: 'true',
PAYLOAD_CI_DEPENDENCY_CHECKER: 'true',
},
async redirects() {
return [

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"private": true,
"type": "module",
"scripts": {
@@ -10,46 +10,46 @@
"build:app": "next build",
"build:app:analyze": "cross-env ANALYZE=true next build",
"build:clean": "pnpm clean:build",
"build:core": "turbo build --filter \"!@payloadcms/plugin-*\"",
"build:core:force": "pnpm clean:build && turbo build --filter \"!@payloadcms/plugin-*\" --no-cache --force",
"build:core": "turbo build --filter \"!@payloadcms/plugin-*\" --filter \"!@payloadcms/storage-*\"",
"build:core:force": "pnpm clean:build && pnpm build:core --no-cache --force",
"build:create-payload-app": "turbo build --filter create-payload-app",
"build:db-mongodb": "turbo build --filter db-mongodb",
"build:db-postgres": "turbo build --filter db-postgres",
"build:db-sqlite": "turbo build --filter db-sqlite",
"build:db-vercel-postgres": "turbo build --filter db-vercel-postgres",
"build:drizzle": "turbo build --filter drizzle",
"build:email-nodemailer": "turbo build --filter email-nodemailer",
"build:email-resend": "turbo build --filter email-resend",
"build:eslint-config": "turbo build --filter eslint-config",
"build:db-mongodb": "turbo build --filter \"@payloadcms/db-mongodb\"",
"build:db-postgres": "turbo build --filter \"@payloadcms/db-postgres\"",
"build:db-sqlite": "turbo build --filter \"@payloadcms/db-sqlite\"",
"build:db-vercel-postgres": "turbo build --filter \"@payloadcms/db-vercel-postgres\"",
"build:drizzle": "turbo build --filter \"@payloadcms/drizzle\"",
"build:email-nodemailer": "turbo build --filter \"@payloadcms/email-nodemailer\"",
"build:email-resend": "turbo build --filter \"@payloadcms/email-resend\"",
"build:eslint-config": "turbo build --filter \"@payloadcms/eslint-config\"",
"build:essentials:force": "pnpm clean:build && turbo build --filter=\"payload...\" --filter=\"@payloadcms/ui\" --filter=\"@payloadcms/next\" --filter=\"@payloadcms/db-mongodb\" --filter=\"@payloadcms/db-postgres\" --filter=\"@payloadcms/richtext-lexical\" --filter=\"@payloadcms/translations\" --filter=\"@payloadcms/plugin-cloud\" --filter=\"@payloadcms/graphql\" --no-cache --force",
"build:force": "pnpm run build:core:force",
"build:graphql": "turbo build --filter graphql",
"build:live-preview": "turbo build --filter live-preview",
"build:live-preview-react": "turbo build --filter live-preview-react",
"build:live-preview-vue": "turbo build --filter live-preview-vue",
"build:graphql": "turbo build --filter \"@payloadcms/graphql\"",
"build:live-preview": "turbo build --filter \"@payloadcms/live-preview\"",
"build:live-preview-react": "turbo build --filter \"@payloadcms/live-preview-react\"",
"build:live-preview-vue": "turbo build --filter \"@payloadcms/live-preview-vue\"",
"build:next": "turbo build --filter \"@payloadcms/next\"",
"build:payload": "turbo build --filter payload",
"build:plugin-cloud": "turbo build --filter plugin-cloud",
"build:plugin-cloud-storage": "turbo build --filter plugin-cloud-storage",
"build:plugin-form-builder": "turbo build --filter plugin-form-builder",
"build:plugin-nested-docs": "turbo build --filter plugin-nested-docs",
"build:plugin-redirects": "turbo build --filter plugin-redirects",
"build:plugin-relationship-object-ids": "turbo build --filter plugin-relationship-object-ids",
"build:plugin-search": "turbo build --filter plugin-search",
"build:plugin-sentry": "turbo build --filter plugin-sentry",
"build:plugin-seo": "turbo build --filter plugin-seo",
"build:plugin-stripe": "turbo build --filter plugin-stripe",
"build:plugin-cloud": "turbo build --filter \"@payloadcms/plugin-cloud\"",
"build:plugin-cloud-storage": "turbo build --filter \"@payloadcms/plugin-cloud-storage\"",
"build:plugin-form-builder": "turbo build --filter \"@payloadcms/plugin-form-builder\"",
"build:plugin-nested-docs": "turbo build --filter \"@payloadcms/plugin-nested-docs\"",
"build:plugin-redirects": "turbo build --filter \"@payloadcms/plugin-redirects\"",
"build:plugin-relationship-object-ids": "turbo build --filter \"@payloadcms/plugin-relationship-object-ids\"",
"build:plugin-search": "turbo build --filter \"@payloadcms/plugin-search\"",
"build:plugin-sentry": "turbo build --filter \"@payloadcms/plugin-sentry\"",
"build:plugin-seo": "turbo build --filter \"@payloadcms/plugin-seo\"",
"build:plugin-stripe": "turbo build --filter \"@payloadcms/plugin-stripe\"",
"build:plugins": "turbo build --filter \"@payloadcms/plugin-*\"",
"build:richtext-lexical": "turbo build --filter richtext-lexical",
"build:richtext-slate": "turbo build --filter richtext-slate",
"build:storage-azure": "turbo build --filter storage-azure",
"build:storage-gcs": "turbo build --filter storage-gcs",
"build:storage-s3": "turbo build --filter storage-s3",
"build:storage-uploadthing": "turbo build --filter storage-uploadthing",
"build:storage-vercel-blob": "turbo build --filter storage-vercel-blob",
"build:richtext-lexical": "turbo build --filter \"@payloadcms/richtext-lexical\"",
"build:richtext-slate": "turbo build --filter \"@payloadcms/richtext-slate\"",
"build:storage-azure": "turbo build --filter \"@payloadcms/storage-azure\"",
"build:storage-gcs": "turbo build --filter \"@payloadcms/storage-gcs\"",
"build:storage-s3": "turbo build --filter \"@payloadcms/storage-s3\"",
"build:storage-uploadthing": "turbo build --filter \"@payloadcms/storage-uploadthing\"",
"build:storage-vercel-blob": "turbo build --filter \"@payloadcms/storage-vercel-blob\"",
"build:tests": "pnpm --filter payload-test-suite run typecheck",
"build:translations": "turbo build --filter translations",
"build:ui": "turbo build --filter ui",
"build:translations": "turbo build --filter \"@payloadcms/translations\"",
"build:ui": "turbo build --filter \"@payloadcms/ui\"",
"clean": "turbo clean",
"clean:all": "node ./scripts/delete-recursively.js '@node_modules' 'media/*' '**/dist/' '**/.cache/*' '**/.next/*' '**/.turbo/*' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
"clean:build": "node ./scripts/delete-recursively.js 'media/' '**/dist/' '**/.cache/' '**/.next/' '**/.turbo/' '**/tsconfig.tsbuildinfo' '**/payload*.tgz' '**/meta_*.json'",
@@ -159,7 +159,7 @@
"swc-plugin-transform-remove-imports": "1.15.0",
"tempy": "1.0.1",
"tsx": "4.17.0",
"turbo": "^2.0.14",
"turbo": "^2.1.0",
"typescript": "5.5.4"
},
"peerDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -58,9 +58,17 @@ type Args = {
tableName: string
timestamps?: boolean
versions: boolean
/**
* Tracks whether or not this table is built
* from the result of a localized array or block field at some point
*/
withinLocalizedArrayOrBlock?: boolean
}
type Result = {
hasLocalizedManyNumberField: boolean
hasLocalizedManyTextField: boolean
hasLocalizedRelationshipField: boolean
hasManyNumberField: 'index' | boolean
hasManyTextField: 'index' | boolean
relationsToBuild: RelationMap
@@ -81,6 +89,7 @@ export const buildTable = ({
tableName,
timestamps,
versions,
withinLocalizedArrayOrBlock,
}: Args): Result => {
const isRoot = !incomingRootTableName
const rootTableName = incomingRootTableName || tableName
@@ -128,6 +137,7 @@ export const buildTable = ({
rootTableIDColType: rootTableIDColType || idColType,
rootTableName,
versions,
withinLocalizedArrayOrBlock,
})
// split the relationsToBuild by localized and non-localized
@@ -478,5 +488,12 @@ export const buildTable = ({
return result
})
return { hasManyNumberField, hasManyTextField, relationsToBuild }
return {
hasLocalizedManyNumberField,
hasLocalizedManyTextField,
hasLocalizedRelationshipField,
hasManyNumberField,
hasManyTextField,
relationsToBuild,
}
}

View File

@@ -52,6 +52,11 @@ type Args = {
rootTableIDColType: IDType
rootTableName: string
versions: boolean
/**
* Tracks whether or not this table is built
* from the result of a localized array or block field at some point
*/
withinLocalizedArrayOrBlock?: boolean
}
type Result = {
@@ -84,6 +89,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
versions,
withinLocalizedArrayOrBlock,
}: Args): Result => {
let hasLocalizedField = false
let hasLocalizedRelationshipField = false
@@ -150,7 +156,11 @@ export const traverseFields = ({
switch (field.type) {
case 'text': {
if (field.hasMany) {
if (field.localized) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
if (isLocalized) {
hasLocalizedManyTextField = true
}
@@ -179,7 +189,11 @@ export const traverseFields = ({
case 'number': {
if (field.hasMany) {
if (field.localized) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
if (isLocalized) {
hasLocalizedManyNumberField = true
}
@@ -255,7 +269,11 @@ export const traverseFields = ({
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
}
if (field.localized) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
if (isLocalized) {
baseColumns.locale = text('locale', { enum: locales }).notNull()
baseExtraConfig.localeIdx = (cols) =>
index(`${selectTableName}_locale_idx`).on(cols.locale)
@@ -337,13 +355,20 @@ export const traverseFields = ({
_parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID),
}
if (field.localized && adapter.payload.config.localization) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
if (isLocalized) {
baseColumns._locale = text('_locale', { enum: locales }).notNull()
baseExtraConfig._localeIdx = (cols) =>
index(`${arrayTableName}_locale_idx`).on(cols._locale)
}
const {
hasLocalizedManyNumberField: subHasLocalizedManyNumberField,
hasLocalizedManyTextField: subHasLocalizedManyTextField,
hasLocalizedRelationshipField: subHasLocalizedRelationshipField,
hasManyNumberField: subHasManyNumberField,
hasManyTextField: subHasManyTextField,
relationsToBuild: subRelationsToBuild,
@@ -360,8 +385,21 @@ export const traverseFields = ({
rootTableName,
tableName: arrayTableName,
versions,
withinLocalizedArrayOrBlock: isLocalized,
})
if (subHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = subHasLocalizedManyNumberField
}
if (subHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = subHasLocalizedRelationshipField
}
if (subHasLocalizedManyTextField) {
hasLocalizedManyTextField = subHasLocalizedManyTextField
}
if (subHasManyTextField) {
if (!hasManyTextField || subHasManyTextField === 'index')
hasManyTextField = subHasManyTextField
@@ -453,13 +491,20 @@ export const traverseFields = ({
_pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path),
}
if (field.localized && adapter.payload.config.localization) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
if (isLocalized) {
baseColumns._locale = text('_locale', { enum: locales }).notNull()
baseExtraConfig._localeIdx = (cols) =>
index(`${blockTableName}_locale_idx`).on(cols._locale)
}
const {
hasLocalizedManyNumberField: subHasLocalizedManyNumberField,
hasLocalizedManyTextField: subHasLocalizedManyTextField,
hasLocalizedRelationshipField: subHasLocalizedRelationshipField,
hasManyNumberField: subHasManyNumberField,
hasManyTextField: subHasManyTextField,
relationsToBuild: subRelationsToBuild,
@@ -476,8 +521,21 @@ export const traverseFields = ({
rootTableName,
tableName: blockTableName,
versions,
withinLocalizedArrayOrBlock: isLocalized,
})
if (subHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = subHasLocalizedManyNumberField
}
if (subHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = subHasLocalizedRelationshipField
}
if (subHasLocalizedManyTextField) {
hasLocalizedManyTextField = subHasLocalizedManyTextField
}
if (subHasManyTextField) {
if (!hasManyTextField || subHasManyTextField === 'index')
hasManyTextField = subHasManyTextField
@@ -577,6 +635,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
versions,
withinLocalizedArrayOrBlock,
})
if (groupHasLocalizedField) hasLocalizedField = true
@@ -618,6 +677,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
versions,
withinLocalizedArrayOrBlock,
})
if (groupHasLocalizedField) hasLocalizedField = true
@@ -660,6 +720,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
versions,
withinLocalizedArrayOrBlock,
})
if (tabHasLocalizedField) hasLocalizedField = true
@@ -702,6 +763,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
versions,
withinLocalizedArrayOrBlock,
})
if (rowHasLocalizedField) hasLocalizedField = true
@@ -753,7 +815,10 @@ export const traverseFields = ({
}
break
}
if (adapter.payload.config.localization && field.localized) {
if (
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
) {
hasLocalizedRelationshipField = true
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

@@ -54,9 +54,17 @@ type Args = {
tableName: string
timestamps?: boolean
versions: boolean
/**
* Tracks whether or not this table is built
* from the result of a localized array or block field at some point
*/
withinLocalizedArrayOrBlock?: boolean
}
type Result = {
hasLocalizedManyNumberField: boolean
hasLocalizedManyTextField: boolean
hasLocalizedRelationshipField: boolean
hasManyNumberField: 'index' | boolean
hasManyTextField: 'index' | boolean
relationsToBuild: RelationMap
@@ -76,6 +84,7 @@ export const buildTable = ({
tableName,
timestamps,
versions,
withinLocalizedArrayOrBlock,
}: Args): Result => {
const isRoot = !incomingRootTableName
const rootTableName = incomingRootTableName || tableName
@@ -122,6 +131,7 @@ export const buildTable = ({
rootTableIDColType: rootTableIDColType || idColType,
rootTableName,
versions,
withinLocalizedArrayOrBlock,
})
// split the relationsToBuild by localized and non-localized
@@ -464,5 +474,12 @@ export const buildTable = ({
return result
})
return { hasManyNumberField, hasManyTextField, relationsToBuild }
return {
hasLocalizedManyNumberField,
hasLocalizedManyTextField,
hasLocalizedRelationshipField,
hasManyNumberField,
hasManyTextField,
relationsToBuild,
}
}

View File

@@ -58,6 +58,11 @@ type Args = {
rootTableIDColType: string
rootTableName: string
versions: boolean
/**
* Tracks whether or not this table is built
* from the result of a localized array or block field at some point
*/
withinLocalizedArrayOrBlock?: boolean
}
type Result = {
@@ -89,6 +94,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
versions,
withinLocalizedArrayOrBlock,
}: Args): Result => {
const throwValidationError = true
let hasLocalizedField = false
@@ -156,7 +162,11 @@ export const traverseFields = ({
switch (field.type) {
case 'text': {
if (field.hasMany) {
if (field.localized) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
if (isLocalized) {
hasLocalizedManyTextField = true
}
@@ -185,7 +195,11 @@ export const traverseFields = ({
case 'number': {
if (field.hasMany) {
if (field.localized) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
if (isLocalized) {
hasLocalizedManyNumberField = true
}
@@ -276,7 +290,11 @@ export const traverseFields = ({
parentIdx: (cols) => index(`${selectTableName}_parent_idx`).on(cols.parent),
}
if (field.localized) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
if (isLocalized) {
baseColumns.locale = adapter.enums.enum__locales('locale').notNull()
baseExtraConfig.localeIdx = (cols) =>
index(`${selectTableName}_locale_idx`).on(cols.locale)
@@ -354,13 +372,20 @@ export const traverseFields = ({
_parentIDIdx: (cols) => index(`${arrayTableName}_parent_id_idx`).on(cols._parentID),
}
if (field.localized && adapter.payload.config.localization) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
if (isLocalized) {
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
baseExtraConfig._localeIdx = (cols) =>
index(`${arrayTableName}_locale_idx`).on(cols._locale)
}
const {
hasLocalizedManyNumberField: subHasLocalizedManyNumberField,
hasLocalizedManyTextField: subHasLocalizedManyTextField,
hasLocalizedRelationshipField: subHasLocalizedRelationshipField,
hasManyNumberField: subHasManyNumberField,
hasManyTextField: subHasManyTextField,
relationsToBuild: subRelationsToBuild,
@@ -377,8 +402,21 @@ export const traverseFields = ({
rootTableName,
tableName: arrayTableName,
versions,
withinLocalizedArrayOrBlock: isLocalized,
})
if (subHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = subHasLocalizedManyNumberField
}
if (subHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = subHasLocalizedRelationshipField
}
if (subHasLocalizedManyTextField) {
hasLocalizedManyTextField = subHasLocalizedManyTextField
}
if (subHasManyTextField) {
if (!hasManyTextField || subHasManyTextField === 'index')
hasManyTextField = subHasManyTextField
@@ -466,13 +504,20 @@ export const traverseFields = ({
_pathIdx: (cols) => index(`${blockTableName}_path_idx`).on(cols._path),
}
if (field.localized && adapter.payload.config.localization) {
const isLocalized =
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
if (isLocalized) {
baseColumns._locale = adapter.enums.enum__locales('_locale').notNull()
baseExtraConfig._localeIdx = (cols) =>
index(`${blockTableName}_locale_idx`).on(cols._locale)
}
const {
hasLocalizedManyNumberField: subHasLocalizedManyNumberField,
hasLocalizedManyTextField: subHasLocalizedManyTextField,
hasLocalizedRelationshipField: subHasLocalizedRelationshipField,
hasManyNumberField: subHasManyNumberField,
hasManyTextField: subHasManyTextField,
relationsToBuild: subRelationsToBuild,
@@ -489,8 +534,21 @@ export const traverseFields = ({
rootTableName,
tableName: blockTableName,
versions,
withinLocalizedArrayOrBlock: isLocalized,
})
if (subHasLocalizedManyNumberField) {
hasLocalizedManyNumberField = subHasLocalizedManyNumberField
}
if (subHasLocalizedRelationshipField) {
hasLocalizedRelationshipField = subHasLocalizedRelationshipField
}
if (subHasLocalizedManyTextField) {
hasLocalizedManyTextField = subHasLocalizedManyTextField
}
if (subHasManyTextField) {
if (!hasManyTextField || subHasManyTextField === 'index')
hasManyTextField = subHasManyTextField
@@ -589,6 +647,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
versions,
withinLocalizedArrayOrBlock,
})
if (groupHasLocalizedField) hasLocalizedField = true
@@ -629,6 +688,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
versions,
withinLocalizedArrayOrBlock,
})
if (groupHasLocalizedField) hasLocalizedField = true
@@ -670,6 +730,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
versions,
withinLocalizedArrayOrBlock,
})
if (tabHasLocalizedField) hasLocalizedField = true
@@ -711,6 +772,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
versions,
withinLocalizedArrayOrBlock,
})
if (rowHasLocalizedField) hasLocalizedField = true
@@ -761,7 +823,11 @@ export const traverseFields = ({
}
break
}
if (adapter.payload.config.localization && field.localized) {
if (
Boolean(field.localized && adapter.payload.config.localization) ||
withinLocalizedArrayOrBlock
) {
hasLocalizedRelationshipField = true
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign */
import type { NumberField } from 'payload'
type Args = {
@@ -6,10 +5,29 @@ type Args = {
locale?: string
numberRows: Record<string, unknown>[]
ref: Record<string, unknown>
withinArrayOrBlockLocale?: string
}
export const transformHasManyNumber = ({ field, locale, numberRows, ref }: Args) => {
const result = numberRows.map(({ number }) => number)
export const transformHasManyNumber = ({
field,
locale,
numberRows,
ref,
withinArrayOrBlockLocale,
}: Args) => {
let result: unknown[]
if (withinArrayOrBlockLocale) {
result = numberRows.reduce((acc, { locale, number }) => {
if (locale === withinArrayOrBlockLocale) {
acc.push(number)
}
return acc
}, [])
} else {
result = numberRows.map(({ number }) => number)
}
if (locale) {
ref[field.name][locale] = result

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign */
import type { TextField } from 'payload'
type Args = {
@@ -6,10 +5,29 @@ type Args = {
locale?: string
ref: Record<string, unknown>
textRows: Record<string, unknown>[]
withinArrayOrBlockLocale?: string
}
export const transformHasManyText = ({ field, locale, ref, textRows }: Args) => {
const result = textRows.map(({ text }) => text)
export const transformHasManyText = ({
field,
locale,
ref,
textRows,
withinArrayOrBlockLocale,
}: Args) => {
let result: unknown[]
if (withinArrayOrBlockLocale) {
result = textRows.reduce((acc, { locale, text }) => {
if (locale === withinArrayOrBlockLocale) {
acc.push(text)
}
return acc
}, [])
} else {
result = textRows.map(({ text }) => text)
}
if (locale) {
ref[field.name][locale] = result

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign */
import type { RelationshipField, UploadField } from 'payload'
type Args = {
@@ -6,21 +5,31 @@ type Args = {
locale?: string
ref: Record<string, unknown>
relations: Record<string, unknown>[]
withinArrayOrBlockLocale?: string
}
export const transformRelationship = ({ field, locale, ref, relations }: Args) => {
export const transformRelationship = ({
field,
locale,
ref,
relations,
withinArrayOrBlockLocale,
}: Args) => {
let result: unknown
if (!('hasMany' in field) || field.hasMany === false) {
const relation = relations[0]
let relation = relations[0]
if (withinArrayOrBlockLocale) {
relation = relations.find((rel) => rel.locale === withinArrayOrBlockLocale)
}
if (relation) {
// Handle hasOne Poly
if (Array.isArray(field.relationTo)) {
const matchedRelation = Object.entries(relation).find(
([key, val]) =>
val !== null && !['id', 'locale', 'order', 'parent', 'path'].includes(key),
)
const matchedRelation = Object.entries(relation).find(([key, val]) => {
return val !== null && !['id', 'locale', 'order', 'parent', 'path'].includes(key)
})
if (matchedRelation) {
const relationTo = matchedRelation[0].replace('ID', '')
@@ -36,18 +45,26 @@ export const transformRelationship = ({ field, locale, ref, relations }: Args) =
const transformedRelations = []
relations.forEach((relation) => {
let matchedLocale = true
if (withinArrayOrBlockLocale) {
matchedLocale = relation.locale === withinArrayOrBlockLocale
}
// Handle hasMany
if (!Array.isArray(field.relationTo)) {
const relatedData = relation[`${field.relationTo}ID`]
if (relatedData) {
if (relatedData && matchedLocale) {
transformedRelations.push(relatedData)
}
} else {
// Handle hasMany Poly
const matchedRelation = Object.entries(relation).find(
([key, val]) =>
val !== null && !['id', 'locale', 'order', 'parent', 'path'].includes(key),
val !== null &&
!['id', 'locale', 'order', 'parent', 'path'].includes(key) &&
matchedLocale,
)
if (matchedRelation) {

View File

@@ -58,6 +58,10 @@ type TraverseFieldsArgs = {
* All hasMany text fields, as returned by Drizzle, keyed on an object by field path
*/
texts: Record<string, Record<string, unknown>[]>
/**
* Set to a locale if this group of fields is within a localized array or block.
*/
withinArrayOrBlockLocale?: string
}
// Traverse fields recursively, transforming data
@@ -75,6 +79,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
relationships,
table,
texts,
withinArrayOrBlockLocale,
}: TraverseFieldsArgs): T => {
const sanitizedPath = path ? `${path}.` : path
@@ -93,6 +98,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
relationships,
table,
texts,
withinArrayOrBlockLocale,
})
}
@@ -114,6 +120,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
relationships,
table,
texts,
withinArrayOrBlockLocale,
})
}
@@ -157,6 +164,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
relationships,
table: row,
texts,
withinArrayOrBlockLocale: locale,
})
if ('_order' in rowResult) {
@@ -192,6 +200,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
relationships,
table: row,
texts,
withinArrayOrBlockLocale,
})
})
}
@@ -237,6 +246,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
relationships,
table: row,
texts,
withinArrayOrBlockLocale: locale,
})
delete blockResult._order
@@ -247,7 +257,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
})
})
} else {
result[field.name] = blocks[blockFieldPath].map((row, i) => {
result[field.name] = blocks[blockFieldPath].reduce((acc, row, i) => {
delete row._order
if (row._uuid) {
row.id = row._uuid
@@ -256,24 +266,40 @@ export const traverseFields = <T extends Record<string, unknown>>({
const block = field.blocks.find(({ slug }) => slug === row.blockType)
if (block) {
return traverseFields<T>({
adapter,
blocks,
config,
dataRef: row,
deletions,
fieldPrefix: '',
fields: block.fields,
numbers,
path: `${blockFieldPath}.${i}`,
relationships,
table: row,
texts,
})
if (
!withinArrayOrBlockLocale ||
(withinArrayOrBlockLocale && withinArrayOrBlockLocale === row._locale)
) {
if (row._locale) {
delete row._locale
}
acc.push(
traverseFields<T>({
adapter,
blocks,
config,
dataRef: row,
deletions,
fieldPrefix: '',
fields: block.fields,
numbers,
path: `${blockFieldPath}.${i}`,
relationships,
table: row,
texts,
withinArrayOrBlockLocale,
}),
)
return acc
}
} else {
acc.push({})
}
return {}
})
return acc
}, [])
}
}
@@ -334,6 +360,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
field,
ref: result,
relations: relationPathMatch,
withinArrayOrBlockLocale,
})
}
return result
@@ -368,6 +395,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
field,
ref: result,
textRows: textPathMatch,
withinArrayOrBlockLocale,
})
}
@@ -402,6 +430,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
field,
numberRows: numberPathMatch,
ref: result,
withinArrayOrBlockLocale,
})
}
@@ -466,6 +495,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
relationships,
table,
texts,
withinArrayOrBlockLocale,
})
if ('_order' in ref) {

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign */
import type { ArrayField } from 'payload'
import type { DrizzleAdapter } from '../../types.js'
@@ -26,6 +25,11 @@ type Args = {
[tableName: string]: Record<string, unknown>[]
}
texts: Record<string, unknown>[]
/**
* Set to a locale code if this set of fields is traversed within a
* localized array or block field
*/
withinArrayOrBlockLocale?: string
}
export const transformArray = ({
@@ -43,6 +47,7 @@ export const transformArray = ({
relationshipsToDelete,
selects,
texts,
withinArrayOrBlockLocale,
}: Args) => {
const newRows: ArrayRowToInsert[] = []
@@ -78,6 +83,10 @@ export const transformArray = ({
newRow.row._locale = locale
}
if (withinArrayOrBlockLocale) {
newRow.row._locale = withinArrayOrBlockLocale
}
traverseFields({
adapter,
arrays: newRow.arrays,
@@ -97,6 +106,7 @@ export const transformArray = ({
row: newRow.row,
selects,
texts,
withinArrayOrBlockLocale,
})
newRows.push(newRow)

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-param-reassign */
import type { BlockField } from 'payload'
import toSnakeCase from 'to-snake-case'
@@ -26,6 +25,11 @@ type Args = {
[tableName: string]: Record<string, unknown>[]
}
texts: Record<string, unknown>[]
/**
* Set to a locale code if this set of fields is traversed within a
* localized array or block field
*/
withinArrayOrBlockLocale?: string
}
export const transformBlocks = ({
adapter,
@@ -41,6 +45,7 @@ export const transformBlocks = ({
relationshipsToDelete,
selects,
texts,
withinArrayOrBlockLocale,
}: Args) => {
data.forEach((blockRow, i) => {
if (typeof blockRow.blockType !== 'string') return
@@ -60,6 +65,7 @@ export const transformBlocks = ({
}
if (field.localized && locale) newRow.row._locale = locale
if (withinArrayOrBlockLocale) newRow.row._locale = withinArrayOrBlockLocale
const blockTableName = adapter.tableNameMap.get(`${baseTableName}_blocks_${blockType}`)
@@ -94,6 +100,7 @@ export const transformBlocks = ({
row: newRow.row,
selects,
texts,
withinArrayOrBlockLocale,
})
blocks[blockType].push(newRow)

View File

@@ -57,6 +57,11 @@ type Args = {
[tableName: string]: Record<string, unknown>[]
}
texts: Record<string, unknown>[]
/**
* Set to a locale code if this set of fields is traversed within a
* localized array or block field
*/
withinArrayOrBlockLocale?: string
}
export const traverseFields = ({
@@ -80,6 +85,7 @@ export const traverseFields = ({
row,
selects,
texts,
withinArrayOrBlockLocale,
}: Args) => {
fields.forEach((field) => {
let columnName = ''
@@ -116,6 +122,7 @@ export const traverseFields = ({
relationshipsToDelete,
selects,
texts,
withinArrayOrBlockLocale: localeKey,
})
arrays[arrayTableName] = arrays[arrayTableName].concat(newRows)
@@ -137,6 +144,7 @@ export const traverseFields = ({
relationshipsToDelete,
selects,
texts,
withinArrayOrBlockLocale,
})
arrays[arrayTableName] = arrays[arrayTableName].concat(newRows)
@@ -168,6 +176,7 @@ export const traverseFields = ({
relationshipsToDelete,
selects,
texts,
withinArrayOrBlockLocale: localeKey,
})
}
})
@@ -186,6 +195,7 @@ export const traverseFields = ({
relationshipsToDelete,
selects,
texts,
withinArrayOrBlockLocale,
})
}
@@ -217,6 +227,7 @@ export const traverseFields = ({
row,
selects,
texts,
withinArrayOrBlockLocale: localeKey,
})
})
} else {
@@ -240,6 +251,7 @@ export const traverseFields = ({
row,
selects,
texts,
withinArrayOrBlockLocale,
})
}
}
@@ -274,6 +286,7 @@ export const traverseFields = ({
row,
selects,
texts,
withinArrayOrBlockLocale: localeKey,
})
})
} else {
@@ -297,6 +310,7 @@ export const traverseFields = ({
row,
selects,
texts,
withinArrayOrBlockLocale,
})
}
}
@@ -321,6 +335,7 @@ export const traverseFields = ({
row,
selects,
texts,
withinArrayOrBlockLocale,
})
}
})
@@ -347,6 +362,7 @@ export const traverseFields = ({
row,
selects,
texts,
withinArrayOrBlockLocale,
})
}
@@ -387,6 +403,7 @@ export const traverseFields = ({
transformRelationship({
baseRow: {
locale: withinArrayOrBlockLocale,
path: relationshipPath,
},
data: fieldData,
@@ -440,6 +457,7 @@ export const traverseFields = ({
} else if (Array.isArray(fieldData)) {
transformTexts({
baseRow: {
locale: withinArrayOrBlockLocale,
path: textPath,
},
data: fieldData,
@@ -471,6 +489,7 @@ export const traverseFields = ({
} else if (Array.isArray(fieldData)) {
transformNumbers({
baseRow: {
locale: withinArrayOrBlockLocale,
path: numberPath,
},
data: fieldData,
@@ -503,6 +522,7 @@ export const traverseFields = ({
const newRows = transformSelects({
id: data._uuid || data.id,
data: data[field.name],
locale: withinArrayOrBlockLocale,
})
selects[selectTableName] = selects[selectTableName].concat(newRows)

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"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.0.0-beta.91",
"version": "3.0.0-beta.95",
"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.0.0-beta.91",
"version": "3.0.0-beta.95",
"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.0.0-beta.91",
"version": "3.0.0-beta.95",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -88,7 +88,7 @@ export const DefaultNavClient: React.FC = () => {
LinkWithDefault) as typeof LinkWithDefault.default
const LinkElement = Link || 'a'
const activeCollection = pathname.endsWith(href)
const activeCollection = pathname.startsWith(href)
return (
<LinkElement

View File

@@ -1,6 +1,6 @@
import type { MappedComponent, ServerProps, VisibleEntities } from 'payload'
import { AppHeader, EntityVisibilityProvider, NavToggler } from '@payloadcms/ui'
import { AppHeader, BulkUploadProvider, EntityVisibilityProvider, NavToggler } from '@payloadcms/ui'
import { RenderComponent, getCreateMappedComponent } from '@payloadcms/ui/shared'
import React from 'react'
@@ -59,21 +59,23 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
return (
<EntityVisibilityProvider visibleEntities={visibleEntities}>
<div>
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
<NavToggler className={`${baseClass}__nav-toggler`}>
<NavHamburger />
</NavToggler>
</div>
<Wrapper baseClass={baseClass} className={className}>
<RenderComponent mappedComponent={MappedDefaultNav} />
<div className={`${baseClass}__wrap`}>
<AppHeader />
{children}
<BulkUploadProvider>
<div>
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
<NavToggler className={`${baseClass}__nav-toggler`}>
<NavHamburger />
</NavToggler>
</div>
</Wrapper>
</div>
<Wrapper baseClass={baseClass} className={className}>
<RenderComponent mappedComponent={MappedDefaultNav} />
<div className={`${baseClass}__wrap`}>
<AppHeader />
{children}
</div>
</Wrapper>
</div>
</BulkUploadProvider>
</EntityVisibilityProvider>
)
}

View File

@@ -280,6 +280,7 @@ export const Document: React.FC<AdminViewProps> = async ({
initialData={data}
initialState={formState}
isEditing={isEditing}
key={locale?.code}
>
{!RootViewOverride && (
<DocumentHeader

View File

@@ -4,7 +4,6 @@ import type { ClientCollectionConfig } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import {
BulkUploadDrawer,
Button,
DeleteMany,
EditMany,
@@ -14,7 +13,6 @@ import {
ListSelection,
Pagination,
PerPage,
PopupList,
PublishMany,
RelationshipProvider,
RenderComponent,
@@ -24,7 +22,7 @@ import {
Table,
UnpublishMany,
ViewDescription,
bulkUploadDrawerSlug,
useBulkUpload,
useConfig,
useEditDepth,
useListInfo,
@@ -60,6 +58,8 @@ export const DefaultListView: React.FC = () => {
const { searchParams } = useSearchParams()
const { openModal } = useModal()
const { clearRouteCache } = useRouteCache()
const { setCollectionSlug, setOnSuccess } = useBulkUpload()
const { drawerSlug } = useBulkUpload()
const { getEntityConfig } = useConfig()
@@ -106,6 +106,12 @@ export const DefaultListView: React.FC = () => {
})
}
const openBulkUpload = React.useCallback(() => {
setCollectionSlug(collectionSlug)
openModal(drawerSlug)
setOnSuccess(clearRouteCache)
}, [clearRouteCache, collectionSlug, drawerSlug, openModal, setCollectionSlug, setOnSuccess])
useEffect(() => {
if (drawerDepth <= 1) {
setStepNav([
@@ -116,6 +122,8 @@ export const DefaultListView: React.FC = () => {
}
}, [setStepNav, labels, drawerDepth])
const isBulkUploadEnabled = isUploadCollection && collectionConfig.upload.bulkUpload
return (
<div className={`${baseClass} ${baseClass}--${collectionSlug}`}>
<SetViewActions actions={actions} />
@@ -126,23 +134,15 @@ export const DefaultListView: React.FC = () => {
<ListHeader heading={getTranslation(labels?.plural, i18n)}>
{hasCreatePermission && (
<Button
Link={Link}
SubMenuPopupContent={
isUploadCollection && collectionConfig.upload.bulkUpload ? (
<PopupList.ButtonGroup>
<PopupList.Button onClick={() => openModal(bulkUploadDrawerSlug)}>
{t('upload:bulkUpload')}
</PopupList.Button>
</PopupList.ButtonGroup>
) : null
}
Link={!isBulkUploadEnabled ? Link : undefined}
aria-label={i18n.t('general:createNewLabel', {
label: getTranslation(labels?.singular, i18n),
})}
buttonStyle="pill"
el="link"
el={!isBulkUploadEnabled ? 'link' : 'button'}
onClick={isBulkUploadEnabled ? openBulkUpload : undefined}
size="small"
to={newDocumentURL}
to={!isBulkUploadEnabled ? newDocumentURL : undefined}
>
{i18n.t('general:createNew')}
</Button>
@@ -155,12 +155,6 @@ export const DefaultListView: React.FC = () => {
<ViewDescription Description={Description} description={description} />
</div>
)}
{isUploadCollection && collectionConfig.upload.bulkUpload ? (
<BulkUploadDrawer
collectionSlug={collectionSlug}
onSuccess={() => clearRouteCache()}
/>
) : null}
</ListHeader>
)}
<ListControls collectionConfig={collectionConfig} fields={fields} />

View File

@@ -37,6 +37,8 @@ export const DeviceContainer: React.FC<{
x = '-50%'
if (
desiredSize &&
measuredDeviceSize &&
typeof zoom === 'number' &&
typeof desiredSize.width === 'number' &&
typeof desiredSize.height === 'number' &&

View File

@@ -100,7 +100,7 @@ const RenderFieldsToDiff: React.FC<Props> = ({
)
}
if (field.type === 'tabs' && 'fields' in field) {
if (field.type === 'tabs' && 'tabs' in field) {
const Tabs = diffComponents.tabs
return (

View File

@@ -66,7 +66,8 @@ export const VersionsViewClient: React.FC<{
limit={data.limit}
nextPage={data.nextPage}
numberOfNeighbors={1}
onChange={() => handlePageChange}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onChange={handlePageChange}
page={data.page}
prevPage={data.prevPage}
totalPages={data.totalPages}
@@ -81,7 +82,8 @@ export const VersionsViewClient: React.FC<{
{i18n.t('general:of')} {data.totalDocs}
</div>
<PerPage
handleChange={() => handlePerPageChange}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
handleChange={handlePerPageChange}
limit={limit ? Number(limit) : 10}
limits={paginationLimits}
/>

View File

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

View File

@@ -7,6 +7,7 @@ import type { BinScript } from '../config/types.js'
import { findConfig } from '../config/find.js'
import { generateImportMap } from './generateImportMap/index.js'
import { generateTypes } from './generateTypes.js'
import { info } from './info.js'
import { loadEnv } from './loadEnv.js'
import { migrate } from './migrate.js'
@@ -16,6 +17,11 @@ export const bin = async () => {
const args = minimist(process.argv.slice(2))
const script = (typeof args._[0] === 'string' ? args._[0] : '').toLowerCase()
if (script === 'info') {
await info()
return
}
if (script === 'run') {
const scriptPath = args._[1]
if (!scriptPath) {

View File

@@ -0,0 +1,63 @@
import { execFileSync } from 'child_process'
import os from 'os'
import { getDependencies } from '../index.js'
import { PAYLOAD_PACKAGE_LIST } from '../versions/payloadPackageList.js'
export const info = async () => {
const deps = await getDependencies(process.cwd(), [
...PAYLOAD_PACKAGE_LIST,
'next',
'react',
'react-dom',
])
const formattedDeps = Array.from(deps.resolved.entries()).map(([name, { version }]) => ({
name,
version,
}))
console.log(generateOutput(formattedDeps))
}
function generateOutput(packages: Array<{ name: string; version: string }>) {
const cpuCores = os.cpus().length
const primaryDeps = packages.filter(({ name }) => name === 'payload' || name === 'next')
const otherDeps = packages
.filter(({ name }) => name !== 'payload' && name !== 'next')
.sort((a, b) => a.name.localeCompare(b.name))
const formattedDeps = [...primaryDeps, ...otherDeps]
.map(({ name, version }) => ` ${name}: ${version}`)
.join('\n')
return `
Binaries:
Node: ${process.versions.node}
npm: ${getBinaryVersion('npm')}
Yarn: ${getBinaryVersion('yarn')}
pnpm: ${getBinaryVersion('pnpm')}
Relevant Packages:
${formattedDeps}
Operating System:
Platform: ${os.platform()}
Arch: ${os.arch()}
Version: ${os.version()}
Available memory (MB): ${Math.ceil(os.totalmem() / 1024 / 1024)}
Available CPU cores: ${cpuCores > 0 ? cpuCores : 'N/A'}
`
}
function getBinaryVersion(binaryName: string) {
try {
return execFileSync(binaryName, ['--version']).toString().trim()
} catch {
return 'N/A'
}
}
// Direct execution
if (import.meta.url === `file://${process.argv[1]}`) {
void info()
}

View File

@@ -0,0 +1,59 @@
import type { CustomVersionParser } from './utilities/dependencies/dependencyChecker.js'
import { checkDependencies } from './utilities/dependencies/dependencyChecker.js'
import { PAYLOAD_PACKAGE_LIST } from './versions/payloadPackageList.js'
const customReactVersionParser: CustomVersionParser = (version) => {
const [mainVersion, ...preReleases] = version.split('-')
if (preReleases?.length === 3) {
// Needs different handling, as it's in a format like 19.0.0-rc-06d0b89e-20240801 format
const date = preReleases[2]
const parts = mainVersion.split('.').map(Number)
return { parts, preReleases: [date] }
}
const parts = mainVersion.split('.').map(Number)
return { parts, preReleases }
}
export async function checkPayloadDependencies() {
const dependencies = [...PAYLOAD_PACKAGE_LIST]
if (process.env.PAYLOAD_CI_DEPENDENCY_CHECKER !== 'true') {
dependencies.push('@payloadcms/plugin-sentry')
}
// First load. First check if there are mismatching dependency versions of payload packages
await checkDependencies({
dependencyGroups: [
{
name: 'payload',
dependencies,
targetVersionDependency: 'payload',
},
{
name: 'react',
dependencies: ['react', 'react-dom'],
targetVersionDependency: 'react',
},
],
dependencyVersions: {
next: {
required: false,
version: '>=15.0.0-canary.104',
},
react: {
customVersionParser: customReactVersionParser,
required: false,
version: '>=19.0.0-rc-06d0b89e-20240801',
},
'react-dom': {
customVersionParser: customReactVersionParser,
required: false,
version: '>=19.0.0-rc-06d0b89e-20240801',
},
},
})
}

View File

@@ -14,6 +14,15 @@ export type ServerOnlyCollectionAdminProperties = keyof Pick<
'hidden' | 'preview'
>
export type ServerOnlyUploadProperties = keyof Pick<
SanitizedCollectionConfig['upload'],
| 'adminThumbnail'
| 'externalFileHeaderFilter'
| 'handlers'
| 'modifyResponseHeaders'
| 'withMetadata'
>
export type ClientCollectionConfig = {
_isPreviewEnabled?: true
admin: {

View File

@@ -39,8 +39,9 @@ export type ClientConfig = {
Logo: MappedComponent
}
}
dependencies?: Record<string, MappedComponent>
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
} & Omit<SanitizedConfig['admin'], 'components' | 'livePreview'>
} & Omit<SanitizedConfig['admin'], 'components' | 'dependencies' | 'livePreview'>
collections: ClientCollectionConfig[]
custom?: Record<string, any>
globals: ClientGlobalConfig[]

View File

@@ -115,11 +115,18 @@ export type FieldHookArgs<TData extends TypeWithID = any, TValue = any, TSibling
/** The collection which the field belongs to. If the field belongs to a global, this will be null. */
collection: SanitizedCollectionConfig | null
context: RequestContext
/**
* Only available in `afterRead` hooks
*/
currentDepth?: number /**
* Only available in `afterRead` hooks
*/
/** The data passed to update the document within create and update operations, and the full document itself in the afterRead hook. */
data?: Partial<TData>
/**
* Only available in the `afterRead` hook.
*/
depth?: number
draft?: boolean
/** The field which the hook is running against. */
field: FieldAffectingData
@@ -1600,7 +1607,7 @@ export function optionIsValue(option: Option): option is string {
export function fieldSupportsMany<TField extends ClientField | Field>(
field: TField,
): field is TField & (TField extends ClientField ? FieldWithManyClient : FieldWithMany) {
return field.type === 'select' || field.type === 'relationship'
return field.type === 'select' || field.type === 'relationship' || field.type === 'upload'
}
export function fieldHasMaxDepth<TField extends ClientField | Field>(

View File

@@ -204,7 +204,9 @@ export const promise = async ({
const hookedValue = await currentHook({
collection,
context,
currentDepth,
data: doc,
depth,
draft,
field,
findMany,
@@ -231,7 +233,9 @@ export const promise = async ({
const hookedValue = await currentHook({
collection,
context,
currentDepth,
data: doc,
depth,
draft,
field,
findMany,

View File

@@ -57,11 +57,12 @@ import type { TypeWithVersion } from './versions/types.js'
import { decrypt, encrypt } from './auth/crypto.js'
import { APIKeyAuthentication } from './auth/strategies/apiKey.js'
import { JWTAuthentication } from './auth/strategies/jwt.js'
import { checkPayloadDependencies } from './checkPayloadDependencies.js'
import localOperations from './collections/operations/local/index.js'
import { consoleEmailAdapter } from './email/consoleEmailAdapter.js'
import { fieldAffectsData } from './fields/config/types.js'
import localGlobalOperations from './globals/operations/local/index.js'
import { getDependencies } from './utilities/dependencies/getDependencies.js'
import { checkDependencies } from './utilities/dependencies/dependencyChecker.js'
import flattenFields from './utilities/flattenTopLevelFields.js'
import { getLogger } from './utilities/logger.js'
import { serverInit as serverInitTelemetry } from './utilities/telemetry/events/serverInit.js'
@@ -430,58 +431,7 @@ export class BasePayload {
process.env.NODE_ENV !== 'production' &&
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER !== 'true'
) {
// First load. First check if there are mismatching dependency versions of payload packages
const resolvedDependencies = await getDependencies(dirname, [
'@payloadcms/ui/shared',
'payload',
'@payloadcms/next/utilities',
'@payloadcms/richtext-lexical',
'@payloadcms/richtext-slate',
'@payloadcms/graphql',
'@payloadcms/plugin-cloud',
'@payloadcms/db-mongodb',
'@payloadcms/db-postgres',
'@payloadcms/plugin-form-builder',
'@payloadcms/plugin-nested-docs',
'@payloadcms/plugin-seo',
'@payloadcms/plugin-search',
'@payloadcms/plugin-cloud-storage',
'@payloadcms/plugin-stripe',
'@payloadcms/plugin-zapier',
'@payloadcms/plugin-redirects',
'@payloadcms/plugin-sentry',
'@payloadcms/bundler-webpack',
'@payloadcms/bundler-vite',
'@payloadcms/live-preview',
'@payloadcms/live-preview-react',
'@payloadcms/translations',
'@payloadcms/email-nodemailer',
'@payloadcms/email-resend',
'@payloadcms/storage-azure',
'@payloadcms/storage-s3',
'@payloadcms/storage-gcs',
'@payloadcms/storage-vercel-blob',
'@payloadcms/storage-uploadthing',
])
// Go through each resolved dependency. If any dependency has a mismatching version, throw an error
const foundVersions: {
[version: string]: string
} = {}
for (const [_pkg, { version }] of resolvedDependencies.resolved) {
if (!Object.keys(foundVersions).includes(version)) {
foundVersions[version] = _pkg
}
}
if (Object.keys(foundVersions).length > 1) {
const formattedVersionsWithPackageNameString = Object.entries(foundVersions)
.map(([version, pkg]) => `${pkg}@${version}`)
.join(', ')
throw new Error(
`Mismatching payload dependency versions found: ${formattedVersionsWithPackageNameString}. All payload and @payloadcms/* packages must have the same version. This is an error with your set-up, caused by you, not a bug in payload. Please go to your package.json and ensure all payload and @payloadcms/* packages have the same version.`,
)
}
await checkPayloadDependencies()
}
this.importMap = options.importMap
@@ -713,6 +663,7 @@ export type { ClientCollectionConfig } from './collections/config/client.js'
export type {
ServerOnlyCollectionAdminProperties,
ServerOnlyCollectionProperties,
ServerOnlyUploadProperties,
} from './collections/config/client.js'
export type {
AfterChangeHook as CollectionAfterChangeHook,
@@ -1047,6 +998,7 @@ export {
deepMergeWithReactComponents,
deepMergeWithSourceArrays,
} from './utilities/deepMerge.js'
export { getDependencies } from './utilities/dependencies/getDependencies.js'
export { default as flattenTopLevelFields } from './utilities/flattenTopLevelFields.js'
export { formatLabels, formatNames, toWords } from './utilities/formatLabels.js'
export { getCollectionIDFieldTypes } from './utilities/getCollectionIDFieldTypes.js'
@@ -1061,7 +1013,7 @@ export { mapAsync } from './utilities/mapAsync.js'
export { mergeListSearchAndWhere } from './utilities/mergeListSearchAndWhere.js'
export { buildVersionCollectionFields } from './versions/buildCollectionFields.js'
export { buildVersionGlobalFields } from './versions/buildGlobalFields.js'
export { getDependencies }
export { checkDependencies }
export { versionDefaults } from './versions/defaults.js'
export { deleteCollectionVersions } from './versions/deleteCollectionVersions.js'
export { enforceMaxVersions } from './versions/enforceMaxVersions.js'

View File

@@ -0,0 +1,114 @@
import path from 'path'
import { fileURLToPath } from 'url'
import { getDependencies } from '../../index.js'
import { compareVersions } from './versionUtils.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export type CustomVersionParser = (version: string) => { parts: number[]; preReleases: string[] }
export type DependencyCheckerArgs = {
/**
* Define dependency groups to ensure that all dependencies within that group are on the same version, and that no dependencies in that group with different versions are found
*/
dependencyGroups?: {
dependencies: string[]
/**
* Name of the dependency group to be displayed in the error message
*/
name: string
targetVersion?: string
targetVersionDependency?: string
}[]
/**
* Dependency package names keyed to their required versions. Supports >= (greater or equal than version) as a prefix, or no prefix for the exact version
*/
dependencyVersions?: {
[dependency: string]: {
customVersionParser?: CustomVersionParser
required?: boolean
version?: string
}
}
}
export async function checkDependencies({
dependencyGroups,
dependencyVersions,
}: DependencyCheckerArgs): Promise<void> {
if (dependencyGroups?.length) {
for (const dependencyGroup of dependencyGroups) {
const resolvedDependencies = await getDependencies(dirname, dependencyGroup.dependencies)
// Go through each resolved dependency. If any dependency has a mismatching version, throw an error
const foundVersions: {
[version: string]: string
} = {}
for (const [_pkg, { version }] of resolvedDependencies.resolved) {
if (!Object.keys(foundVersions).includes(version)) {
foundVersions[version] = _pkg
}
}
if (Object.keys(foundVersions).length > 1) {
const targetVersion =
dependencyGroup.targetVersion ??
resolvedDependencies.resolved.get(dependencyGroup.targetVersionDependency)?.version
if (targetVersion) {
const formattedVersionsWithPackageNameString = Object.entries(foundVersions)
.filter(([version]) => version !== targetVersion)
.map(([version, pkg]) => `${pkg}@${version} (Please change this to ${targetVersion})`)
.join(', ')
throw new Error(
`Mismatching "${dependencyGroup.name}" dependency versions found: ${formattedVersionsWithPackageNameString}. All "${dependencyGroup.name}" packages must have the same version. This is an error with your set-up, not a bug in Payload. Please go to your package.json and ensure all "${dependencyGroup.name}" packages have the same version.`,
)
} else {
const formattedVersionsWithPackageNameString = Object.entries(foundVersions)
.map(([version, pkg]) => `${pkg}@${version}`)
.join(', ')
throw new Error(
`Mismatching "${dependencyGroup.name}" dependency versions found: ${formattedVersionsWithPackageNameString}. All "${dependencyGroup.name}" packages must have the same version. This is an error with your set-up, not a bug in Payload. Please go to your package.json and ensure all "${dependencyGroup.name}" packages have the same version.`,
)
}
}
}
}
if (dependencyVersions && Object.keys(dependencyVersions).length) {
const resolvedDependencies = await getDependencies(dirname, Object.keys(dependencyVersions))
for (const [dependency, settings] of Object.entries(dependencyVersions)) {
const resolvedDependency = resolvedDependencies.resolved.get(dependency)
if (!resolvedDependency) {
if (!settings.required) {
continue
}
throw new Error(`Dependency ${dependency} not found. Please ensure it is installed.`)
}
if (settings.version) {
const settingsVersionToCheck = settings.version.startsWith('>=')
? settings.version.slice(2)
: settings.version
const versionCompareResult = compareVersions(
resolvedDependency.version,
settingsVersionToCheck,
settings.customVersionParser,
)
if (settings.version.startsWith('>=')) {
if (versionCompareResult === 'lower') {
throw new Error(
`Dependency ${dependency} is on version ${resolvedDependency.version}, but ${settings.version} or greater is required. Please update this dependency.`,
)
}
} else if (versionCompareResult === 'lower' || versionCompareResult === 'greater') {
throw new Error(
`Dependency ${dependency} is on version ${resolvedDependency.version}, but ${settings.version} is required. Please update this dependency.`,
)
}
}
}
}
}

View File

@@ -0,0 +1,75 @@
import type { CustomVersionParser } from './dependencyChecker.js'
export function parseVersion(version: string): { parts: number[]; preReleases: string[] } {
const [mainVersion, ...preReleases] = version.split('-')
const parts = mainVersion.split('.').map(Number)
return { parts, preReleases }
}
function extractNumbers(str: string): number[] {
const matches = str.match(/\d+/g) || []
return matches.map(Number)
}
function comparePreRelease(v1: string, v2: string): number {
const num1 = extractNumbers(v1)
const num2 = extractNumbers(v2)
for (let i = 0; i < Math.max(num1.length, num2.length); i++) {
if ((num1[i] || 0) < (num2[i] || 0)) return -1
if ((num1[i] || 0) > (num2[i] || 0)) return 1
}
// If numeric parts are equal, compare the whole string
if (v1 < v2) return -1
if (v1 > v2) return 1
return 0
}
/**
* Compares two semantic version strings, including handling pre-release identifiers.
*
* This function first compares the major, minor, and patch components as integers.
* If these components are equal, it then moves on to compare pre-release versions.
* Pre-release versions are compared first by extracting and comparing any numerical values.
* If numerical values are equal, it compares the whole pre-release string lexicographically.
*
* @param {string} compare - The first version string to compare.
* @param {string} to - The second version string to compare.
* @param {function} [customVersionParser] - An optional function to parse version strings into parts and pre-releases.
* @returns {string} - Returns greater if compare is greater than to, lower if compare is less than to, and equal if they are equal.
*/
export function compareVersions(
compare: string,
to: string,
customVersionParser?: CustomVersionParser,
): 'equal' | 'greater' | 'lower' {
const { parts: parts1, preReleases: preReleases1 } = customVersionParser
? customVersionParser(compare)
: parseVersion(compare)
const { parts: parts2, preReleases: preReleases2 } = customVersionParser
? customVersionParser(to)
: parseVersion(to)
// Compare main version parts
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
if ((parts1[i] || 0) > (parts2[i] || 0)) return 'greater'
if ((parts1[i] || 0) < (parts2[i] || 0)) return 'lower'
}
// Compare pre-release parts if main versions are equal
if (preReleases1?.length || preReleases2?.length) {
for (let i = 0; i < Math.max(preReleases1.length, preReleases2.length); i++) {
if (!preReleases1[i]) return 'greater'
if (!preReleases2[i]) return 'lower'
const result = comparePreRelease(preReleases1[i], preReleases2[i])
if (result !== 0) {
return result === 1 ? 'greater' : 'lower'
}
// Equal => continue for loop to check for next pre-release part
}
}
return 'equal'
}

View File

@@ -1,4 +1,4 @@
import type { FormState } from 'payload'
import type { FormState } from '../admin/types.js'
import { unflatten } from './unflatten.js'

View File

@@ -0,0 +1,31 @@
export const PAYLOAD_PACKAGE_LIST = [
'payload',
'@payloadcms/bundler-vite',
'@payloadcms/bundler-webpack',
'@payloadcms/db-mongodb',
'@payloadcms/db-postgres',
'@payloadcms/email-nodemailer',
'@payloadcms/email-resend',
'@payloadcms/graphql',
'@payloadcms/live-preview-react',
'@payloadcms/live-preview',
'@payloadcms/next/utilities',
'@payloadcms/plugin-cloud-storage',
'@payloadcms/plugin-cloud',
'@payloadcms/plugin-form-builder',
'@payloadcms/plugin-nested-docs',
'@payloadcms/plugin-redirects',
'@payloadcms/plugin-search',
'@payloadcms/plugin-seo',
'@payloadcms/plugin-stripe',
'@payloadcms/plugin-zapier',
'@payloadcms/richtext-lexical',
'@payloadcms/richtext-slate',
'@payloadcms/storage-azure',
'@payloadcms/storage-gcs',
'@payloadcms/storage-s3',
'@payloadcms/storage-uploadthing',
'@payloadcms/storage-vercel-blob',
'@payloadcms/translations',
'@payloadcms/ui/shared',
]

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"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.0.0-beta.91",
"version": "3.0.0-beta.95",
"description": "Redirects plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-relationship-object-ids",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"description": "A Payload plugin to store all relationship IDs as ObjectIDs",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-search",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"description": "Search plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-seo",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"description": "SEO plugin for Payload",
"keywords": [
"payload",
@@ -56,18 +56,18 @@
"lint:fix": "eslint --fix --ext .ts,.tsx src",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"dependencies": {
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/next": "workspace:*",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"@types/react": "npm:types-react@19.0.0-rc.0",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0",
"payload": "workspace:*"
},
"peerDependencies": {
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"payload": "workspace:*",
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801",
"react-dom": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801"

View File

@@ -1,6 +1,7 @@
'use client'
import type { FieldType, Options, UploadFieldProps } from '@payloadcms/ui'
import type { FieldType, Options } from '@payloadcms/ui'
import type { UploadFieldProps } from 'payload'
import {
FieldLabel,
@@ -156,6 +157,7 @@ export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
setValue(null)
}
}}
path={field.path}
relationTo={relationTo}
required={required}
serverURL={serverURL}

View File

@@ -1,6 +1,5 @@
import type { Config, GroupField, TabsField, TextField } from 'payload'
import { addDataAndFileToRequest } from '@payloadcms/next/utilities'
import { deepMergeSimple } from 'payload/shared'
import type {
@@ -133,7 +132,11 @@ export const seoPlugin =
...(config.endpoints ?? []),
{
handler: async (req) => {
await addDataAndFileToRequest(req)
const data = await req.json()
if (data) {
req.data = data
}
const result = pluginConfig.generateTitle
? await pluginConfig.generateTitle({
@@ -148,7 +151,11 @@ export const seoPlugin =
},
{
handler: async (req) => {
await addDataAndFileToRequest(req)
const data = await req.json()
if (data) {
req.data = data
}
const result = pluginConfig.generateDescription
? await pluginConfig.generateDescription({
@@ -163,7 +170,11 @@ export const seoPlugin =
},
{
handler: async (req) => {
await addDataAndFileToRequest(req)
const data = await req.json()
if (data) {
req.data = data
}
const result = pluginConfig.generateURL
? await pluginConfig.generateURL({
@@ -178,7 +189,11 @@ export const seoPlugin =
},
{
handler: async (req) => {
await addDataAndFileToRequest(req)
const data = await req.json()
if (data) {
req.data = data
}
const result = pluginConfig.generateImage
? await pluginConfig.generateImage({

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-stripe",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"description": "Stripe plugin for Payload",
"keywords": [
"payload",
@@ -54,6 +54,7 @@
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"dependencies": {
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"lodash.get": "^4.4.2",
"stripe": "^10.2.0",
@@ -62,8 +63,6 @@
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/next": "workspace:*",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"@types/express": "^4.17.9",
"@types/lodash.get": "^4.4.7",
"@types/react": "npm:types-react@19.0.0-rc.0",
@@ -72,8 +71,6 @@
"payload": "workspace:*"
},
"peerDependencies": {
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"payload": "workspace:*"
},
"publishConfig": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"description": "The officially supported Lexical richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -59,9 +59,12 @@
"@lexical/rich-text": "0.17.0",
"@lexical/selection": "0.17.0",
"@lexical/utils": "0.17.0",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"@types/uuid": "10.0.0",
"bson-objectid": "2.0.4",
"dequal": "2.0.3",
"escape-html": "1.0.3",
"lexical": "0.17.0",
"react-error-boundary": "4.0.13",
"uuid": "10.0.0"
@@ -74,9 +77,7 @@
"@babel/preset-typescript": "^7.24.1",
"@lexical/eslint-plugin": "0.17.0",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/next": "workspace:*",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"@types/escape-html": "1.0.4",
"@types/json-schema": "7.0.15",
"@types/node": "20.12.5",
"@types/react": "npm:types-react@19.0.0-rc.0",
@@ -103,8 +104,6 @@
"@lexical/table": "0.17.0",
"@lexical/utils": "0.17.0",
"@payloadcms/next": "workspace:*",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"lexical": "0.17.0",
"payload": "workspace:*",
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801",

View File

@@ -6,8 +6,8 @@ import { QuoteNode } from '@lexical/rich-text'
import { createServerFeature } from '../../../utilities/createServerFeature.js'
import { convertLexicalNodesToHTML } from '../../converters/html/converter/index.js'
import { createNode } from '../../typeUtilities.js'
import { i18n } from './i18n.js'
import { MarkdownTransformer } from '../markdownTransformer.js'
import { i18n } from './i18n.js'
export type SerializedQuoteNode = Spread<
{
@@ -28,6 +28,8 @@ export const BlockquoteFeature = createServerFeature({
html: {
converter: async ({
converters,
currentDepth,
depth,
draft,
node,
overrideAccess,
@@ -37,6 +39,8 @@ export const BlockquoteFeature = createServerFeature({
}) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
currentDepth,
depth,
draft,
lexicalNodes: node.children,
overrideAccess,

View File

@@ -5,9 +5,21 @@ import type { HTMLConverter } from '../types.js'
import { convertLexicalNodesToHTML } from '../index.js'
export const ParagraphHTMLConverter: HTMLConverter<SerializedParagraphNode> = {
async converter({ converters, draft, node, overrideAccess, parent, req, showHiddenFields }) {
async converter({
converters,
currentDepth,
depth,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) {
const childrenText = await convertLexicalNodesToHTML({
converters,
currentDepth,
depth,
draft,
lexicalNodes: node.children,
overrideAccess,

View File

@@ -1,12 +1,14 @@
import type { SerializedTextNode } from 'lexical'
import escapeHTML from 'escape-html'
import type { HTMLConverter } from '../types.js'
import { NodeFormat } from '../../../../../lexical/utils/nodeFormat.js'
export const TextHTMLConverter: HTMLConverter<SerializedTextNode> = {
converter({ node }) {
let text = node.text
let text = escapeHTML(node.text)
if (node.format & NodeFormat.IS_BOLD) {
text = `<strong>${text}</strong>`

View File

@@ -7,7 +7,9 @@ import type { HTMLConverter, SerializedLexicalNodeWithParent } from './types.js'
export type ConvertLexicalToHTMLArgs = {
converters: HTMLConverter[]
currentDepth?: number
data: SerializedEditorState
depth?: number
draft?: boolean // default false
overrideAccess?: boolean // default false
showHiddenFields?: boolean // default false
@@ -42,7 +44,9 @@ export type ConvertLexicalToHTMLArgs = {
export async function convertLexicalToHTML({
converters,
currentDepth,
data,
depth,
draft,
overrideAccess,
payload,
@@ -54,8 +58,18 @@ export async function convertLexicalToHTML({
req = await createLocalReq({}, payload)
}
if (!currentDepth) {
currentDepth = 0
}
if (!depth) {
depth = req?.payload?.config?.defaultDepth
}
return await convertLexicalNodesToHTML({
converters,
currentDepth,
depth,
draft: draft === undefined ? false : draft,
lexicalNodes: data?.root?.children,
overrideAccess: overrideAccess === undefined ? false : overrideAccess,
@@ -69,6 +83,8 @@ export async function convertLexicalToHTML({
export async function convertLexicalNodesToHTML({
converters,
currentDepth,
depth,
draft,
lexicalNodes,
overrideAccess,
@@ -77,6 +93,8 @@ export async function convertLexicalNodesToHTML({
showHiddenFields,
}: {
converters: HTMLConverter[]
currentDepth: number
depth: number
draft: boolean
lexicalNodes: SerializedLexicalNode[]
overrideAccess: boolean
@@ -100,6 +118,8 @@ export async function convertLexicalNodesToHTML({
return await unknownConverter.converter({
childIndex: i,
converters,
currentDepth,
depth,
draft,
node,
overrideAccess,
@@ -113,6 +133,8 @@ export async function convertLexicalNodesToHTML({
return await converterForNode.converter({
childIndex: i,
converters,
currentDepth,
depth,
draft,
node,
overrideAccess,

View File

@@ -5,6 +5,8 @@ export type HTMLConverter<T extends SerializedLexicalNode = SerializedLexicalNod
converter: (args: {
childIndex: number
converters: HTMLConverter<any>[]
currentDepth: number
depth: number
draft: boolean
node: T
overrideAccess: boolean

View File

@@ -162,6 +162,8 @@ export const lexicalHTML: (
afterRead: [
async ({
collection,
currentDepth,
depth,
draft,
field,
global,
@@ -217,7 +219,9 @@ export const lexicalHTML: (
return await convertLexicalToHTML({
converters: finalConverters,
currentDepth,
data: lexicalFieldData,
depth,
draft,
overrideAccess,
req,

View File

@@ -73,6 +73,8 @@ export const EXPERIMENTAL_TableFeature = createServerFeature({
html: {
converter: async ({
converters,
currentDepth,
depth,
draft,
node,
overrideAccess,
@@ -82,6 +84,8 @@ export const EXPERIMENTAL_TableFeature = createServerFeature({
}) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
currentDepth,
depth,
draft,
lexicalNodes: node.children,
overrideAccess,
@@ -104,6 +108,8 @@ export const EXPERIMENTAL_TableFeature = createServerFeature({
html: {
converter: async ({
converters,
currentDepth,
depth,
draft,
node,
overrideAccess,
@@ -113,6 +119,8 @@ export const EXPERIMENTAL_TableFeature = createServerFeature({
}) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
currentDepth,
depth,
draft,
lexicalNodes: node.children,
overrideAccess,
@@ -144,6 +152,8 @@ export const EXPERIMENTAL_TableFeature = createServerFeature({
html: {
converter: async ({
converters,
currentDepth,
depth,
draft,
node,
overrideAccess,
@@ -153,6 +163,8 @@ export const EXPERIMENTAL_TableFeature = createServerFeature({
}) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
currentDepth,
depth,
draft,
lexicalNodes: node.children,
overrideAccess,

View File

@@ -46,6 +46,8 @@ export const HeadingFeature = createServerFeature<
html: {
converter: async ({
converters,
currentDepth,
depth,
draft,
node,
overrideAccess,
@@ -55,6 +57,8 @@ export const HeadingFeature = createServerFeature<
}) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
currentDepth,
depth,
draft,
lexicalNodes: node.children,
overrideAccess,

View File

@@ -7,7 +7,7 @@ import type {
User,
} from 'payload'
import { validateUrl } from '../../../lexical/utils/url.js'
import { validateUrl, validateUrlMinimal } from '../../../lexical/utils/url.js'
export const getBaseFields = (
config: SanitizedConfig,
@@ -64,10 +64,20 @@ export const getBaseFields = (
{
name: 'url',
type: 'text',
hooks: {
beforeChange: [
({ value }) => {
if (!validateUrl(value)) {
return encodeURIComponent(value)
}
return value
},
],
},
label: ({ t }) => t('fields:enterURL'),
required: true,
validate: (value: string) => {
if (!validateUrl(value)) {
if (!validateUrlMinimal(value)) {
return 'Invalid URL'
}
},

View File

@@ -1,5 +1,6 @@
import type { CollectionSlug, Config, Field, FieldAffectingData, SanitizedConfig } from 'payload'
import escapeHTML from 'escape-html'
import { sanitizeFields } from 'payload'
import { deepCopyObject } from 'payload/shared'
@@ -116,6 +117,8 @@ export const LinkFeature = createServerFeature<
html: {
converter: async ({
converters,
currentDepth,
depth,
draft,
node,
overrideAccess,
@@ -125,6 +128,8 @@ export const LinkFeature = createServerFeature<
}) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
currentDepth,
depth,
draft,
lexicalNodes: node.children,
overrideAccess,
@@ -161,6 +166,8 @@ export const LinkFeature = createServerFeature<
html: {
converter: async ({
converters,
currentDepth,
depth,
draft,
node,
overrideAccess,
@@ -170,6 +177,8 @@ export const LinkFeature = createServerFeature<
}) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
currentDepth,
depth,
draft,
lexicalNodes: node.children,
overrideAccess,
@@ -186,7 +195,7 @@ export const LinkFeature = createServerFeature<
const href: string =
node.fields.linkType === 'custom'
? node.fields.url
? escapeHTML(node.fields.url)
: (node.fields.doc?.value as string)
return `<a href="${href}"${target}${rel}>${childrenText}</a>`

View File

@@ -7,9 +7,21 @@ import type { SerializedListItemNode, SerializedListNode } from './plugin/index.
import { convertLexicalNodesToHTML } from '../converters/html/converter/index.js'
export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
converter: async ({ converters, draft, node, overrideAccess, parent, req, showHiddenFields }) => {
converter: async ({
converters,
currentDepth,
depth,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
currentDepth,
depth,
draft,
lexicalNodes: node.children,
overrideAccess,
@@ -27,11 +39,23 @@ export const ListHTMLConverter: HTMLConverter<SerializedListNode> = {
}
export const ListItemHTMLConverter: HTMLConverter<SerializedListItemNode> = {
converter: async ({ converters, draft, node, overrideAccess, parent, req, showHiddenFields }) => {
converter: async ({
converters,
currentDepth,
depth,
draft,
node,
overrideAccess,
parent,
req,
showHiddenFields,
}) => {
const hasSubLists = node.children.some((child) => child.type === 'list')
const childrenText = await convertLexicalNodesToHTML({
converters,
currentDepth,
depth,
draft,
lexicalNodes: node.children,
overrideAccess,

View File

@@ -41,62 +41,67 @@ export type RelationshipFeatureProps = {
export const RelationshipFeature = createServerFeature<
RelationshipFeatureProps,
RelationshipFeatureProps
RelationshipFeatureProps,
ExclusiveRelationshipFeatureProps
>({
feature: ({ props }) => ({
ClientFeature: '@payloadcms/richtext-lexical/client#RelationshipFeatureClient',
i18n,
nodes: [
createNode({
graphQLPopulationPromises: [relationshipPopulationPromiseHOC(props)],
hooks: {
afterRead: [
({
currentDepth,
depth,
draft,
node,
overrideAccess,
populationPromises,
req,
showHiddenFields,
}) => {
if (!node?.value) {
feature: ({ props }) => {
// we don't need to pass maxDepth to the client, it's only used on the server
const { maxDepth, ...clientFeatureProps } = props ?? {}
return {
ClientFeature: '@payloadcms/richtext-lexical/client#RelationshipFeatureClient',
clientFeatureProps,
i18n,
nodes: [
createNode({
graphQLPopulationPromises: [relationshipPopulationPromiseHOC(props)],
hooks: {
afterRead: [
({
currentDepth,
depth,
draft,
node,
overrideAccess,
populationPromises,
req,
showHiddenFields,
}) => {
if (!node?.value) {
return node
}
const collection = req.payload.collections[node?.relationTo]
if (!collection) {
return node
}
// @ts-expect-error
const id = node?.value?.id || node?.value // for backwards-compatibility
const populateDepth = maxDepth !== undefined && maxDepth < depth ? maxDepth : depth
populationPromises.push(
populate({
id,
collectionSlug: collection.config.slug,
currentDepth,
data: node,
depth: populateDepth,
draft,
key: 'value',
overrideAccess,
req,
showHiddenFields,
}),
)
return node
}
const collection = req.payload.collections[node?.relationTo]
if (!collection) {
return node
}
// @ts-expect-error
const id = node?.value?.id || node?.value // for backwards-compatibility
const populateDepth =
props?.maxDepth !== undefined && props?.maxDepth < depth ? props?.maxDepth : depth
populationPromises.push(
populate({
id,
collectionSlug: collection.config.slug,
currentDepth,
data: node,
depth: populateDepth,
draft,
key: 'value',
overrideAccess,
req,
showHiddenFields,
}),
)
return node
},
],
},
node: RelationshipServerNode,
}),
],
}),
},
],
},
node: RelationshipServerNode,
}),
],
}
},
key: 'relationship',
})

View File

@@ -97,7 +97,15 @@ export const UploadFeature = createServerFeature<
createNode({
converters: {
html: {
converter: async ({ draft, node, overrideAccess, req, showHiddenFields }) => {
converter: async ({
currentDepth,
depth,
draft,
node,
overrideAccess,
req,
showHiddenFields,
}) => {
// @ts-expect-error
const id = node?.value?.id || node?.value // for backwards-compatibility
@@ -110,9 +118,9 @@ export const UploadFeature = createServerFeature<
await populate({
id,
collectionSlug: node.relationTo,
currentDepth: 0,
currentDepth,
data: uploadDocument,
depth: 1,
depth,
draft,
key: 'value',
overrideAccess,

View File

@@ -5,16 +5,14 @@ import type {
SerializedLexicalNode,
} from 'lexical'
import { fileURLToPath } from 'node:url'
import path from 'path'
import {
afterChangeTraverseFields,
afterReadTraverseFields,
beforeChangeTraverseFields,
beforeValidateTraverseFields,
checkDependencies,
deepCopyObject,
deepCopyObjectSimple,
getDependencies,
withNullableJSONSchemaType,
} from 'payload'
@@ -42,46 +40,32 @@ import { richTextValidateHOC } from './validate/index.js'
let defaultSanitizedServerEditorConfig: SanitizedServerEditorConfig = null
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapterProvider {
return async ({ config, isRoot }) => {
if (
process.env.NODE_ENV !== 'production' &&
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER !== 'true'
) {
const resolvedDependencies = await getDependencies(dirname, [
'lexical',
'@lexical/headless',
'@lexical/link',
'@lexical/list',
'@lexical/mark',
'@lexical/markdown',
'@lexical/react',
'@lexical/rich-text',
'@lexical/selection',
'@lexical/utils',
])
// Go through each resolved dependency. If any dependency has a mismatching version, throw an error
const foundVersions: {
[version: string]: string
} = {}
for (const [_pkg, { version }] of resolvedDependencies.resolved) {
if (!Object.keys(foundVersions).includes(version)) {
foundVersions[version] = _pkg
}
}
if (Object.keys(foundVersions).length > 1) {
const formattedVersionsWithPackageNameString = Object.entries(foundVersions)
.map(([version, pkg]) => `${pkg}@${version}`)
.join(', ')
throw new Error(
`Mismatching lexical dependency versions found: ${formattedVersionsWithPackageNameString}. All lexical and @lexical/* packages must have the same version. This is an error with your set-up, caused by you, not a bug in payload. Please go to your package.json and ensure all lexical and @lexical/* packages have the same version.`,
)
}
await checkDependencies({
dependencyGroups: [
{
name: 'lexical',
dependencies: [
'lexical',
'@lexical/headless',
'@lexical/link',
'@lexical/list',
'@lexical/mark',
'@lexical/markdown',
'@lexical/react',
'@lexical/rich-text',
'@lexical/selection',
'@lexical/utils',
],
targetVersion: '0.17.0',
},
],
})
}
let features: FeatureProviderServer<any, any, any>[] = []
@@ -991,8 +975,9 @@ export type * from './nodeTypes.js'
export { defaultRichTextValue } from './populateGraphQL/defaultValue.js'
export { populate } from './populateGraphQL/populate.js'
export type { LexicalEditorProps, LexicalRichTextAdapter } from './types.js'
export { createServerFeature } from './utilities/createServerFeature.js'
export type { FieldsDrawerProps } from './utilities/fieldsDrawer/Drawer.js'
export type { FieldsDrawerProps } from './utilities/fieldsDrawer/Drawer.js'
export { upgradeLexicalData } from './utilities/upgradeLexicalData/index.js'

View File

@@ -25,6 +25,16 @@ const absoluteRegExp =
* */
const relativeOrAnchorRegExp = /^[\w\-./]*(?:#\w[\w-]*)?$/
/**
* Prevents unreasonable URLs from being inserted into the editor.
* @param url
*/
export function validateUrlMinimal(url: string): boolean {
if (!url) return false
return !url.includes(' ')
}
// Do not keep validateUrl function too loose. This is run when pasting in text, to determine if links are in that text and if it should create AutoLinkNodes.
// This is why we do not allow stuff like anchors here, as we don't want copied anchors to be turned into AutoLinkNodes.
export function validateUrl(url: string): boolean {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-slate",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"description": "The officially supported Slate richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -38,6 +38,8 @@
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"dependencies": {
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"is-hotkey": "0.2.0",
"slate": "0.91.4",
"slate-history": "0.86.0",
@@ -46,7 +48,6 @@
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/ui": "workspace:*",
"@types/is-hotkey": "^0.1.10",
"@types/node": "20.12.5",
"@types/react": "npm:types-react@19.0.0-rc.0",
@@ -54,8 +55,6 @@
"payload": "workspace:*"
},
"peerDependencies": {
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"payload": "workspace:*",
"react": "^19.0.0 || ^19.0.0-rc-06d0b89e-20240801"
},

View File

@@ -63,3 +63,4 @@ export { UnderlineLeafButton } from '../../field/leaves/underline/LeafButton.js'
export { UnderlineLeaf } from '../../field/leaves/underline/Underline/index.js'
export { useLeaf } from '../../field/providers/LeafProvider.js'
export { useSlatePlugin } from '../../utilities/useSlatePlugin.js'

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-azure",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"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.0.0-beta.91",
"version": "3.0.0-beta.95",
"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.0.0-beta.91",
"version": "3.0.0-beta.95",
"description": "Payload storage adapter for Amazon S3",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/translations",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/ui",
"version": "3.0.0-beta.91",
"version": "3.0.0-beta.95",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -7,9 +7,22 @@
height: 100%;
padding: calc(var(--base) * 2) var(--gutter-h);
}
.dropzone {
flex-direction: column;
justify-content: center;
display: flex;
gap: var(--base);
background-color: var(--theme-elevation-50);
p {
margin: 0;
}
}
&__dragAndDropText {
margin: 0;
text-transform: lowercase;
align-self: center;
}
}

View File

@@ -3,6 +3,7 @@
import React from 'react'
import { useTranslation } from '../../../providers/Translation/index.js'
import { Button } from '../../Button/index.js'
import { Dropzone } from '../../Dropzone/index.js'
import { DrawerHeader } from '../Header/index.js'
import './index.scss'
@@ -16,11 +17,43 @@ type Props = {
export function AddFilesView({ onCancel, onDrop }: Props) {
const { t } = useTranslation()
const inputRef = React.useRef(null)
return (
<div className={baseClass}>
<DrawerHeader onClose={onCancel} title={t('upload:addFiles')} />
<div className={`${baseClass}__dropArea`}>
<Dropzone multipleFiles onChange={onDrop} />
<Dropzone multipleFiles onChange={onDrop}>
<Button
buttonStyle="pill"
iconPosition="left"
onClick={() => {
if (inputRef.current) {
inputRef.current.click()
}
}}
size="small"
>
{t('upload:selectFile')}
</Button>
<input
aria-hidden="true"
className={`${baseClass}__hidden-input`}
hidden
onChange={(e) => {
if (e.target.files && e.target.files.length > 0) {
onDrop(e.target.files)
}
}}
ref={inputRef}
type="file"
/>
<p className={`${baseClass}__dragAndDropText`}>
{t('general:or')} {t('upload:dragAndDrop')}
</p>
</Dropzone>
{/* <Dropzone multipleFiles onChange={onDrop} /> */}
</div>
</div>
)

View File

@@ -9,7 +9,7 @@ import { useConfig } from '../../../providers/Config/index.js'
import { DocumentInfoProvider } from '../../../providers/DocumentInfo/index.js'
import { useTranslation } from '../../../providers/Translation/index.js'
import { ActionsBar } from '../ActionsBar/index.js'
import { discardBulkUploadModalSlug } from '../DiscardWithoutSaving/index.js'
import { DiscardWithoutSaving, discardBulkUploadModalSlug } from '../DiscardWithoutSaving/index.js'
import { EditForm } from '../EditForm/index.js'
import { FileSidebar } from '../FileSidebar/index.js'
import { useFormsManager } from '../FormsManager/index.js'
@@ -44,20 +44,24 @@ export function AddingFilesView() {
onClose={() => openModal(discardBulkUploadModalSlug)}
title={getTranslation(collection.labels.singular, i18n)}
/>
<DocumentInfoProvider
collectionSlug={collectionSlug}
docPermissions={docPermissions}
hasPublishPermission={hasPublishPermission}
hasSavePermission={hasSavePermission}
id={null}
initialData={reduceFieldsToValues(activeForm.formState, true)}
initialState={activeForm.formState}
key={`${activeIndex}-${forms.length}`}
>
<ActionsBar />
<EditForm submitted={hasSubmitted} />
</DocumentInfoProvider>
{activeForm ? (
<DocumentInfoProvider
collectionSlug={collectionSlug}
docPermissions={docPermissions}
hasPublishPermission={hasPublishPermission}
hasSavePermission={hasSavePermission}
id={null}
initialData={reduceFieldsToValues(activeForm.formState, true)}
initialState={activeForm.formState}
key={`${activeIndex}-${forms.length}`}
>
<ActionsBar />
<EditForm submitted={hasSubmitted} />
</DocumentInfoProvider>
) : null}
</div>
<DiscardWithoutSaving />
</div>
)
}

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