Compare commits
32 Commits
feat/uploa
...
feat/lexic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce8fe5509a | ||
|
|
b47bfce1ad | ||
|
|
de8e345c7b | ||
|
|
701cd2dd97 | ||
|
|
185da7c8c4 | ||
|
|
4e5f03d308 | ||
|
|
85e70338d6 | ||
|
|
d9b77d5c95 | ||
|
|
a311413e8f | ||
|
|
f8c92b5f69 | ||
|
|
673ecf596e | ||
|
|
d44d1d3da4 | ||
|
|
048a168970 | ||
|
|
5f7331cbe4 | ||
|
|
4b9a5ae7c2 | ||
|
|
775c74bc0d | ||
|
|
3693d5c7f4 | ||
|
|
78cdb2cd8c | ||
|
|
f1372d1687 | ||
|
|
70f22da627 | ||
|
|
300bc55635 | ||
|
|
22c8328576 | ||
|
|
f4b0f9bee2 | ||
|
|
5b2b437c62 | ||
|
|
fc07ee80d7 | ||
|
|
4c6161a16e | ||
|
|
369b3fe46d | ||
|
|
ddc2b58f47 | ||
|
|
9b159820e2 | ||
|
|
3af759ea5d | ||
|
|
447f5dd689 | ||
|
|
16d50f62bb |
54
AGENTS.md
54
AGENTS.md
@@ -1,54 +0,0 @@
|
||||
# Payload Monorepo Agent Instructions
|
||||
|
||||
## Project Structure
|
||||
|
||||
- Packages are located in the `packages/` directory.
|
||||
- The main Payload package is `packages/payload`. This contains the core functionality.
|
||||
- Database adapters are in `packages/db-*`.
|
||||
- The UI package is `packages/ui`.
|
||||
- The Next.js integration is in `packages/next`.
|
||||
- Rich text editor packages are in `packages/richtext-*`.
|
||||
- Storage adapters are in `packages/storage-*`.
|
||||
- Email adapters are in `packages/email-*`.
|
||||
- Plugins which add additional functionality are in `packages/plugin-*`.
|
||||
- Documentation is in the `docs/` directory.
|
||||
- Monorepo tooling is in the `tools/` directory.
|
||||
- Test suites and configs are in the `test/` directory.
|
||||
- LLMS.txt is at URL: https://payloadcms.com/llms.txt
|
||||
- LLMS-FULL.txt is at URL: https://payloadcms.com/llms-full.txt
|
||||
|
||||
## Dev environment tips
|
||||
|
||||
- Any package can be built using a `pnpm build:*` script defined in the root `package.json`. These typically follow the format `pnpm build:<directory_name>`. The options are all of the top-level directories inside the `packages/` directory. Ex `pnpm build:db-mongodb` which builds the `packages/db-mongodb` package.
|
||||
- ALL packages can be built with `pnpm build:all`.
|
||||
- Use `pnpm dev` to start the monorepo dev server. This loads the default config located at `test/_community/config.ts`.
|
||||
- Specific dev configs for each package can be run with `pnpm dev <directory_name>`. The options are all of the top-level directories inside the `test/` directory. Ex `pnpm dev fields` which loads the `test/fields/config.ts` config. The directory name can either encompass a single area of functionality or be the name of a specific package.
|
||||
|
||||
## Testing instructions
|
||||
|
||||
- There are unit, integration, and e2e tests in the monorepo.
|
||||
- Unit tests can be run with `pnpm test:unit`.
|
||||
- Integration tests can be run with `pnpm test:int`. Individual test suites can be run with `pnpm test:int <directory_name>`, which will point at `test/<directory_name>/int.spec.ts`.
|
||||
- E2E tests can be run with `pnpm test:e2e`.
|
||||
- All tests can be run with `pnpm test`.
|
||||
- Prefer running `pnpm test:int` for verifying local code changes.
|
||||
|
||||
## PR Guidelines
|
||||
|
||||
- This repository follows conventional commits for PR titles
|
||||
- PR Title format: <type>(<scope>): <title>. Title must start with a lowercase letter.
|
||||
- Valid types are build, chore, ci, docs, examples, feat, fix, perf, refactor, revert, style, templates, test
|
||||
- Prefer `feat` for new features and `fix` for bug fixes.
|
||||
- Valid scopes are the following regex patterns: cpa, db-\*, db-mongodb, db-postgres, db-vercel-postgres, db-sqlite, drizzle, email-\*, email-nodemailer, email-resend, eslint, graphql, live-preview, live-preview-react, next, payload-cloud, plugin-cloud, plugin-cloud-storage, plugin-form-builder, plugin-import-export, plugin-multi-tenant, plugin-nested-docs, plugin-redirects, plugin-search, plugin-sentry, plugin-seo, plugin-stripe, richtext-\*, richtext-lexical, richtext-slate, storage-\*, storage-azure, storage-gcs, storage-uploadthing, storage-vercel-blob, storage-s3, translations, ui, templates, examples(\/(\w|-)+)?, deps
|
||||
- Scopes should be chosen based upon the package(s) being modified. If multiple packages are being modified, choose the most relevant one or no scope at all.
|
||||
- Example PR titles:
|
||||
- `feat(db-mongodb): add support for transactions`
|
||||
- `feat(richtext-lexical): add options to hide block handles`
|
||||
- `fix(ui): json field type ignoring editorOptions`
|
||||
|
||||
## Commit Guidelines
|
||||
|
||||
- This repository follows conventional commits for commit messages
|
||||
- The first commit of a branch should follow the PR title format: <type>(<scope>): <title>. Follow the same rules as PR titles.
|
||||
- Subsequent commits should prefer `chore` commits without a scope unless a specific package is being modified.
|
||||
- These will eventually be squashed into the first commit when merging the PR.
|
||||
@@ -131,29 +131,6 @@ localization: {
|
||||
|
||||
Since the filtering happens at the root level of the application and its result is not calculated every time you navigate to a new page, you may want to call `router.refresh` in a custom component that watches when values that affect the result change. In the example above, you would want to do this when `supportedLocales` changes on the tenant document.
|
||||
|
||||
## Experimental Options
|
||||
|
||||
Experimental options are features that may not be fully stable and may change or be removed in future releases.
|
||||
|
||||
These options can be enabled in your Payload Config under the `experimental` key. You can set them like this:
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
export default buildConfig({
|
||||
// ...
|
||||
experimental: {
|
||||
localizeStatus: true,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The following experimental options are available related to localization:
|
||||
|
||||
| Option | Description |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`localizeStatus`** | **Boolean.** When `true`, shows document status per locale in the admin panel instead of always showing the latest overall status. Opt-in for backwards compatibility. Defaults to `false`. |
|
||||
|
||||
## Field Localization
|
||||
|
||||
Payload Localization works on a **field** level—not a document level. In addition to configuring the base Payload Config to support Localization, you need to specify each field that you would like to localize.
|
||||
|
||||
@@ -70,7 +70,6 @@ The following options are available:
|
||||
| **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](../admin/overview#admin-options). |
|
||||
| **`bin`** | Register custom bin scripts for Payload to execute. [More Details](#custom-bin-scripts). |
|
||||
| **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). |
|
||||
| **`experimental`** | Configure experimental features for Payload. These may be unstable and may change or be removed in future releases. [More details](../experimental). |
|
||||
| **`db`** \* | The Database Adapter which will be used by Payload. [More details](../database/overview). |
|
||||
| **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. |
|
||||
| **`collections`** | An array of Collections for Payload to manage. [More details](./collections). |
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
---
|
||||
title: Experimental Features
|
||||
label: Overview
|
||||
order: 10
|
||||
desc: Enable and configure experimental functionality within Payload. These featuresmay be unstable and may change or be removed without notice.
|
||||
keywords: experimental, unstable, beta, preview, features, configuration, Payload, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
Experimental features allow you to try out new functionality before it becomes a stable part of Payload. These features may still be in active development, may have incomplete functionality, and can change or be removed in future releases without warning.
|
||||
|
||||
## How It Works
|
||||
|
||||
Experimental features are configured via the root-level `experimental` property in your [Payload Config](../configuration/overview). This property contains individual feature flags, each flag can be configured independently, allowing you to selectively opt into specific functionality.
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload'
|
||||
|
||||
const config = buildConfig({
|
||||
// ...
|
||||
experimental: {
|
||||
localizeStatus: true, // highlight-line
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Experimental Options
|
||||
|
||||
The following options are available:
|
||||
|
||||
| Option | Description |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **`localizeStatus`** | **Boolean.** When `true`, shows document status per locale in the admin panel instead of always showing the latest overall status. Opt-in for backwards compatibility. Defaults to `false`. |
|
||||
|
||||
This list may change without notice.
|
||||
|
||||
## When to Use Experimental Features
|
||||
|
||||
You might enable an experimental feature when:
|
||||
|
||||
- You want early access to new capabilities before their stable release.
|
||||
- You can accept the risks of using potentially unstable functionality.
|
||||
- You are testing new features in a development or staging environment.
|
||||
- You wish to provide feedback to the Payload team on new functionality.
|
||||
|
||||
If you are working on a production application, carefully evaluate whether the benefits outweigh the risks. For most stable applications, it is recommended to wait until the feature is officially released.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong> To stay up to date on experimental features or share
|
||||
your feedback, visit the{' '}
|
||||
<a
|
||||
href="https://github.com/payloadcms/payload/discussions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Payload GitHub Discussions
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a
|
||||
href="https://github.com/payloadcms/payload/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
open an issue
|
||||
</a>
|
||||
.
|
||||
</Banner>
|
||||
1
next-env.d.ts
vendored
1
next-env.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -147,8 +147,8 @@
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/minimist": "1.2.5",
|
||||
"@types/node": "22.15.30",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/shelljs": "0.8.15",
|
||||
"chalk": "^4.1.2",
|
||||
"comment-json": "^4.2.3",
|
||||
@@ -175,8 +175,8 @@
|
||||
"playwright": "1.54.1",
|
||||
"playwright-core": "1.54.1",
|
||||
"prettier": "3.5.3",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1",
|
||||
"rimraf": "6.0.1",
|
||||
"sharp": "0.32.6",
|
||||
"shelljs": "0.8.5",
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -12,7 +12,6 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
||||
autosave,
|
||||
createdAt,
|
||||
globalSlug,
|
||||
localeStatus,
|
||||
parent,
|
||||
publishedLocale,
|
||||
req,
|
||||
@@ -34,7 +33,6 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
localeStatus,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
|
||||
@@ -12,7 +12,6 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
autosave,
|
||||
collectionSlug,
|
||||
createdAt,
|
||||
localeStatus,
|
||||
parent,
|
||||
publishedLocale,
|
||||
req,
|
||||
@@ -38,7 +37,6 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
localeStatus,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
|
||||
@@ -179,13 +179,6 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
|
||||
for (let i = 0; i < result.docs.length; i++) {
|
||||
const id = result.docs[i].parent
|
||||
|
||||
const localeStatus = result.docs[i].localeStatus || {}
|
||||
if (locale && localeStatus[locale]) {
|
||||
result.docs[i].status = localeStatus[locale]
|
||||
result.docs[i].version._status = localeStatus[locale]
|
||||
}
|
||||
|
||||
result.docs[i] = result.docs[i].version ?? {}
|
||||
result.docs[i].id = id
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
autosave,
|
||||
createdAt,
|
||||
globalSlug,
|
||||
localeStatus,
|
||||
publishedLocale,
|
||||
req,
|
||||
returning,
|
||||
@@ -36,7 +35,6 @@ export async function createGlobalVersion<T extends TypeWithID>(
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
localeStatus,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
updatedAt,
|
||||
|
||||
@@ -15,7 +15,6 @@ export async function createVersion<T extends TypeWithID>(
|
||||
autosave,
|
||||
collectionSlug,
|
||||
createdAt,
|
||||
localeStatus,
|
||||
parent,
|
||||
publishedLocale,
|
||||
req,
|
||||
@@ -41,7 +40,6 @@ export async function createVersion<T extends TypeWithID>(
|
||||
autosave,
|
||||
createdAt,
|
||||
latest: true,
|
||||
localeStatus,
|
||||
parent,
|
||||
publishedLocale,
|
||||
snapshot,
|
||||
|
||||
@@ -36,17 +36,15 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
where: combinedWhere,
|
||||
})
|
||||
|
||||
for (let i = 0; i < result.docs.length; i++) {
|
||||
const id = result.docs[i].parent
|
||||
const localeStatus = result.docs[i].localeStatus || {}
|
||||
if (locale && localeStatus[locale]) {
|
||||
result.docs[i].status = localeStatus[locale]
|
||||
result.docs[i].version._status = localeStatus[locale]
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
docs: result.docs.map((doc) => {
|
||||
doc = {
|
||||
id: doc.parent,
|
||||
...doc.version,
|
||||
}
|
||||
|
||||
result.docs[i] = result.docs[i].version ?? {}
|
||||
result.docs[i].id = id
|
||||
return doc
|
||||
}),
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-perfectionist": "3.9.1",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-d331ba04-20250307",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-b1b0955f-20250901",
|
||||
"eslint-plugin-regexp": "2.7.0",
|
||||
"globals": "16.0.0",
|
||||
"typescript": "5.7.3",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"eslint-plugin-jest-dom": "5.5.0",
|
||||
"eslint-plugin-jsx-a11y": "6.10.2",
|
||||
"eslint-plugin-perfectionist": "3.9.1",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-d331ba04-20250307",
|
||||
"eslint-plugin-react-hooks": "0.0.0-experimental-b1b0955f-20250901",
|
||||
"eslint-plugin-regexp": "2.7.0",
|
||||
"globals": "16.0.0",
|
||||
"typescript": "5.7.3",
|
||||
|
||||
@@ -46,8 +46,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -120,10 +120,10 @@
|
||||
"@next/eslint-plugin-next": "15.4.4",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/busboy": "1.5.4",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/uuid": "10.0.0",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
"esbuild": "0.25.5",
|
||||
"esbuild-sass-plugin": "3.3.1",
|
||||
"payload": "workspace:*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ServerFunction, ServerFunctionHandler } from 'payload'
|
||||
|
||||
import { copyDataFromLocaleHandler } from '@payloadcms/ui/rsc'
|
||||
import { _internal_renderFieldHandler, copyDataFromLocaleHandler } from '@payloadcms/ui/rsc'
|
||||
import { buildFormStateHandler } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import { buildTableStateHandler } from '@payloadcms/ui/utilities/buildTableState'
|
||||
import { getFolderResultsComponentAndDataHandler } from '@payloadcms/ui/utilities/getFolderResultsComponentAndData'
|
||||
@@ -11,19 +11,26 @@ import { renderDocumentSlotsHandler } from '../views/Document/renderDocumentSlot
|
||||
import { renderListHandler } from '../views/List/handleServerFunction.js'
|
||||
import { initReq } from './initReq.js'
|
||||
|
||||
const serverFunctions: Record<string, ServerFunction> = {
|
||||
const baseServerFunctions: Record<string, ServerFunction<any, any>> = {
|
||||
'copy-data-from-locale': copyDataFromLocaleHandler,
|
||||
'form-state': buildFormStateHandler,
|
||||
'get-folder-results-component-and-data': getFolderResultsComponentAndDataHandler,
|
||||
'render-document': renderDocumentHandler,
|
||||
'render-document-slots': renderDocumentSlotsHandler,
|
||||
'render-field': _internal_renderFieldHandler,
|
||||
'render-list': renderListHandler,
|
||||
'schedule-publish': schedulePublishHandler,
|
||||
'table-state': buildTableStateHandler,
|
||||
}
|
||||
|
||||
export const handleServerFunctions: ServerFunctionHandler = async (args) => {
|
||||
const { name: fnKey, args: fnArgs, config: configPromise, importMap } = args
|
||||
const {
|
||||
name: fnKey,
|
||||
args: fnArgs,
|
||||
config: configPromise,
|
||||
importMap,
|
||||
serverFunctions: extraServerFunctions,
|
||||
} = args
|
||||
|
||||
const { req } = await initReq({
|
||||
configPromise,
|
||||
@@ -37,6 +44,11 @@ export const handleServerFunctions: ServerFunctionHandler = async (args) => {
|
||||
req,
|
||||
}
|
||||
|
||||
const serverFunctions = {
|
||||
...baseServerFunctions,
|
||||
...(extraServerFunctions || {}),
|
||||
}
|
||||
|
||||
const fn = serverFunctions[fnKey]
|
||||
|
||||
if (!fn) {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import type {
|
||||
AdminViewServerProps,
|
||||
SanitizedDocumentPermissions,
|
||||
SanitizedFieldsPermissions,
|
||||
} from 'payload'
|
||||
import type { AdminViewServerProps } from 'payload'
|
||||
|
||||
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||
import React from 'react'
|
||||
|
||||
import { getDocPreferences } from '../Document/getDocPreferences.js'
|
||||
import { getDocumentData } from '../Document/getDocumentData.js'
|
||||
import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
|
||||
import { CreateFirstUserClient } from './index.client.js'
|
||||
import './index.scss'
|
||||
|
||||
@@ -46,27 +43,18 @@ export async function CreateFirstUserView({ initPageResult }: AdminViewServerPro
|
||||
user: req.user,
|
||||
})
|
||||
|
||||
const baseFields: SanitizedFieldsPermissions = Object.fromEntries(
|
||||
collectionConfig.fields
|
||||
.filter((f): f is { name: string } & typeof f => 'name' in f && typeof f.name === 'string')
|
||||
.map((f) => [f.name, { create: true, read: true, update: true }]),
|
||||
)
|
||||
|
||||
// In create-first-user we should always allow all fields
|
||||
const docPermissionsForForm: SanitizedDocumentPermissions = {
|
||||
create: true,
|
||||
delete: true,
|
||||
fields: baseFields,
|
||||
read: true,
|
||||
readVersions: true,
|
||||
update: true,
|
||||
}
|
||||
// Get permissions
|
||||
const { docPermissions } = await getDocumentPermissions({
|
||||
collectionConfig,
|
||||
data,
|
||||
req,
|
||||
})
|
||||
|
||||
// Build initial form state from data
|
||||
const { state: formState } = await buildFormState({
|
||||
collectionSlug: collectionConfig.slug,
|
||||
data,
|
||||
docPermissions: docPermissionsForForm,
|
||||
docPermissions,
|
||||
docPreferences,
|
||||
locale: locale?.code,
|
||||
operation: 'create',
|
||||
@@ -81,7 +69,7 @@ export async function CreateFirstUserView({ initPageResult }: AdminViewServerPro
|
||||
<h1>{req.t('general:welcome')}</h1>
|
||||
<p>{req.t('authentication:beginCreateFirstUser')}</p>
|
||||
<CreateFirstUserClient
|
||||
docPermissions={docPermissionsForForm}
|
||||
docPermissions={docPermissions}
|
||||
docPreferences={docPreferences}
|
||||
initialState={formState}
|
||||
loginWithUsername={loginWithUsername}
|
||||
|
||||
@@ -19,7 +19,6 @@ type AutosaveCellProps = {
|
||||
rowData: {
|
||||
autosave?: boolean
|
||||
id: number | string
|
||||
localeStatus?: Record<string, 'draft' | 'published'>
|
||||
publishedLocale?: string
|
||||
version: {
|
||||
_status: string
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { MarkOptional } from 'ts-essentials'
|
||||
|
||||
import type { RowField, RowFieldClient } from '../../fields/config/types.js'
|
||||
import type {
|
||||
ClientComponentProps,
|
||||
ClientFieldBase,
|
||||
FieldClientComponent,
|
||||
FieldPaths,
|
||||
@@ -21,9 +22,7 @@ import type {
|
||||
|
||||
type RowFieldClientWithoutType = MarkOptional<RowFieldClient, 'type'>
|
||||
|
||||
type RowFieldBaseClientProps = {
|
||||
readonly forceRender?: boolean
|
||||
} & Omit<FieldPaths, 'path'>
|
||||
type RowFieldBaseClientProps = Omit<FieldPaths, 'path'> & Pick<ClientComponentProps, 'forceRender'>
|
||||
|
||||
export type RowFieldClientProps = Omit<ClientFieldBase<RowFieldClientWithoutType>, 'path'> &
|
||||
RowFieldBaseClientProps
|
||||
|
||||
@@ -21,6 +21,13 @@ export type ClientFieldWithOptionalType = MarkOptional<ClientField, 'type'>
|
||||
export type ClientComponentProps = {
|
||||
customComponents?: FormField['customComponents']
|
||||
field: ClientBlock | ClientField | ClientTab
|
||||
/**
|
||||
* Controls the rendering behavior of the fields, i.e. defers rendering until they intersect with the viewport using the Intersection Observer API.
|
||||
*
|
||||
* If true, the fields will be rendered immediately, rather than waiting for them to intersect with the viewport.
|
||||
*
|
||||
* If a number is provided, will immediately render fields _up to that index_.
|
||||
*/
|
||||
forceRender?: boolean
|
||||
permissions?: SanitizedFieldPermissions
|
||||
readOnly?: boolean
|
||||
|
||||
@@ -36,6 +36,31 @@ export type ServerFunctionHandler = (
|
||||
args: {
|
||||
config: Promise<SanitizedConfig> | SanitizedConfig
|
||||
importMap: ImportMap
|
||||
/**
|
||||
* A map of server function names to their implementations. These are
|
||||
* registered alongside the base server functions and can be called
|
||||
* using the useServerFunctions() hook.
|
||||
*
|
||||
* @example
|
||||
* const { serverFunction } = useServerFunctions()
|
||||
*
|
||||
* const callServerFunction = useCallback(() => {
|
||||
*
|
||||
* async function call() {
|
||||
* const result = (await serverFunction({
|
||||
* name: 'record-key',
|
||||
* args: {
|
||||
* // Your args
|
||||
* },
|
||||
* }))
|
||||
*
|
||||
* // Do someting with the result
|
||||
* }
|
||||
*
|
||||
* void call()
|
||||
* }, [serverFunction])
|
||||
*/
|
||||
serverFunctions?: Record<string, ServerFunction<any, any>>
|
||||
} & ServerFunctionClientArgs,
|
||||
) => Promise<unknown>
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { initTransaction } from '../../utilities/initTransaction.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
import { getFieldsToSign } from '../getFieldsToSign.js'
|
||||
import { jwtSign } from '../jwt.js'
|
||||
import { addSessionToUser } from '../sessions.js'
|
||||
import { authenticateLocalStrategy } from '../strategies/local/authenticate.js'
|
||||
import { generatePasswordSaltHash } from '../strategies/local/generatePasswordSaltHash.js'
|
||||
|
||||
@@ -144,25 +143,12 @@ export const resetPasswordOperation = async <TSlug extends CollectionSlug>(
|
||||
|
||||
await authenticateLocalStrategy({ doc, password: data.password })
|
||||
|
||||
const fieldsToSignArgs: Parameters<typeof getFieldsToSign>[0] = {
|
||||
const fieldsToSign = getFieldsToSign({
|
||||
collectionConfig,
|
||||
email: user.email,
|
||||
user,
|
||||
}
|
||||
|
||||
const { sid } = await addSessionToUser({
|
||||
collectionConfig,
|
||||
payload,
|
||||
req,
|
||||
user,
|
||||
})
|
||||
|
||||
if (sid) {
|
||||
fieldsToSignArgs.sid = sid
|
||||
}
|
||||
|
||||
const fieldsToSign = getFieldsToSign(fieldsToSignArgs)
|
||||
|
||||
const { token } = await jwtSign({
|
||||
fieldsToSign,
|
||||
secret,
|
||||
|
||||
@@ -291,7 +291,6 @@ export const createOperation = async <
|
||||
autosave,
|
||||
collection: collectionConfig,
|
||||
docWithLocales: result,
|
||||
locale,
|
||||
operation: 'create',
|
||||
payload,
|
||||
publishSpecificLocale,
|
||||
|
||||
@@ -316,7 +316,6 @@ export const updateDocument = async <
|
||||
collection: collectionConfig,
|
||||
docWithLocales: result,
|
||||
draft: shouldSaveDraft,
|
||||
locale,
|
||||
operation: 'update',
|
||||
payload,
|
||||
publishSpecificLocale,
|
||||
|
||||
@@ -159,16 +159,6 @@ export const createClientConfig = ({
|
||||
|
||||
break
|
||||
|
||||
case 'experimental':
|
||||
if (config.experimental) {
|
||||
clientConfig.experimental = {}
|
||||
if (config.experimental?.localizeStatus) {
|
||||
clientConfig.experimental.localizeStatus = config.experimental.localizeStatus
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'folders':
|
||||
if (config.folders) {
|
||||
clientConfig.folders = {
|
||||
|
||||
@@ -47,7 +47,6 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
|
||||
defaultDepth: 2,
|
||||
defaultMaxTextLength: 40000,
|
||||
endpoints: [],
|
||||
experimental: {},
|
||||
globals: [],
|
||||
graphQL: {
|
||||
disablePlaygroundInProduction: true,
|
||||
@@ -122,7 +121,6 @@ export const addDefaultsToConfig = (config: Config): Config => {
|
||||
config.defaultDepth = config.defaultDepth ?? 2
|
||||
config.defaultMaxTextLength = config.defaultMaxTextLength ?? 40000
|
||||
config.endpoints = config.endpoints ?? []
|
||||
config.experimental = config.experimental ?? {}
|
||||
config.globals = config.globals ?? []
|
||||
config.graphQL = {
|
||||
disableIntrospectionInProduction: true,
|
||||
|
||||
@@ -721,14 +721,6 @@ export type ImportMapGenerators = Array<
|
||||
}) => void
|
||||
>
|
||||
|
||||
/**
|
||||
* Experimental features.
|
||||
* These may be unstable and may change or be removed in future releases.
|
||||
*/
|
||||
export type ExperimentalConfig = {
|
||||
localizeStatus?: boolean
|
||||
}
|
||||
|
||||
export type AfterErrorHook = (
|
||||
args: AfterErrorHookArgs,
|
||||
) => AfterErrorResult | Promise<AfterErrorResult>
|
||||
@@ -1049,12 +1041,6 @@ export type Config = {
|
||||
email?: EmailAdapter | Promise<EmailAdapter>
|
||||
/** Custom REST endpoints */
|
||||
endpoints?: Endpoint[]
|
||||
/**
|
||||
* Configure experimental features for Payload.
|
||||
*
|
||||
* These features may be unstable and may change or be removed in future releases.
|
||||
*/
|
||||
experimental?: ExperimentalConfig
|
||||
/**
|
||||
* Options for folder view within the admin panel
|
||||
* @experimental this feature may change in minor versions until it is fully stable
|
||||
@@ -1323,7 +1309,6 @@ export type SanitizedConfig = {
|
||||
/** Default richtext editor to use for richText fields */
|
||||
editor?: RichTextAdapter<any, any, any>
|
||||
endpoints: Endpoint[]
|
||||
experimental?: ExperimentalConfig
|
||||
globals: SanitizedGlobalConfig[]
|
||||
i18n: Required<I18nOptions>
|
||||
jobs: SanitizedJobsConfig
|
||||
|
||||
@@ -390,7 +390,6 @@ export type CreateVersionArgs<T = TypeWithID> = {
|
||||
autosave: boolean
|
||||
collectionSlug: CollectionSlug
|
||||
createdAt: string
|
||||
localeStatus?: Record<string, 'draft' | 'published'>
|
||||
/** ID of the parent document for which the version should be created for */
|
||||
parent: number | string
|
||||
publishedLocale?: string
|
||||
@@ -415,7 +414,6 @@ export type CreateGlobalVersionArgs<T = TypeWithID> = {
|
||||
autosave: boolean
|
||||
createdAt: string
|
||||
globalSlug: GlobalSlug
|
||||
localeStatus?: Record<string, 'draft' | 'published'>
|
||||
/** ID of the parent document for which the version should be created for */
|
||||
parent: number | string
|
||||
publishedLocale?: string
|
||||
|
||||
@@ -87,8 +87,8 @@ export type RootFoldersConfiguration = {
|
||||
collectionOverrides?: (({
|
||||
collection,
|
||||
}: {
|
||||
collection: Omit<CollectionConfig, 'trash'>
|
||||
}) => Omit<CollectionConfig, 'trash'> | Promise<Omit<CollectionConfig, 'trash'>>)[]
|
||||
collection: CollectionConfig
|
||||
}) => CollectionConfig | Promise<CollectionConfig>)[]
|
||||
/**
|
||||
* If true, you can scope folders to specific collections.
|
||||
*
|
||||
|
||||
@@ -285,7 +285,6 @@ export const updateOperation = async <
|
||||
docWithLocales: result,
|
||||
draft: shouldSaveDraft,
|
||||
global: globalConfig,
|
||||
locale,
|
||||
operation: 'update',
|
||||
payload,
|
||||
publishSpecificLocale,
|
||||
|
||||
@@ -196,17 +196,10 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
|
||||
type: 'group',
|
||||
admin: {
|
||||
hidden: true,
|
||||
...(size.admin?.disableListColumn && { disableListColumn: true }),
|
||||
...(size.admin?.disableListFilter && { disableListFilter: true }),
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
...url,
|
||||
admin: {
|
||||
...url.admin,
|
||||
...(size.admin?.disableListColumn && { disableListColumn: true }),
|
||||
...(size.admin?.disableListFilter && { disableListFilter: true }),
|
||||
},
|
||||
hooks: {
|
||||
afterRead: [
|
||||
({ data, value }) => {
|
||||
@@ -225,45 +218,12 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
...width,
|
||||
admin: {
|
||||
...width.admin,
|
||||
...(size.admin?.disableListColumn && { disableListColumn: true }),
|
||||
...(size.admin?.disableListFilter && { disableListFilter: true }),
|
||||
},
|
||||
},
|
||||
{
|
||||
...height,
|
||||
admin: {
|
||||
...height.admin,
|
||||
...(size.admin?.disableListColumn && { disableListColumn: true }),
|
||||
...(size.admin?.disableListFilter && { disableListFilter: true }),
|
||||
},
|
||||
},
|
||||
{
|
||||
...mimeType,
|
||||
admin: {
|
||||
...mimeType.admin,
|
||||
...(size.admin?.disableListColumn && { disableListColumn: true }),
|
||||
...(size.admin?.disableListFilter && { disableListFilter: true }),
|
||||
},
|
||||
},
|
||||
{
|
||||
...filesize,
|
||||
admin: {
|
||||
...filesize.admin,
|
||||
...(size.admin?.disableListColumn && { disableListColumn: true }),
|
||||
...(size.admin?.disableListFilter && { disableListFilter: true }),
|
||||
},
|
||||
},
|
||||
width,
|
||||
height,
|
||||
mimeType,
|
||||
filesize,
|
||||
{
|
||||
...filename,
|
||||
admin: {
|
||||
...filename.admin,
|
||||
...(size.admin?.disableListColumn && { disableListColumn: true }),
|
||||
...(size.admin?.disableListFilter && { disableListFilter: true }),
|
||||
},
|
||||
unique: false,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -69,10 +69,6 @@ export type GenerateImageName = (args: {
|
||||
}) => string
|
||||
|
||||
export type ImageSize = {
|
||||
admin?: {
|
||||
disableListColumn?: boolean
|
||||
disableListFilter?: boolean
|
||||
}
|
||||
/**
|
||||
* @deprecated prefer position
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @ts-strict-ignore
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { CheckboxField, Field, Option } from '../fields/config/types.js'
|
||||
|
||||
export const statuses: Option[] = [
|
||||
@@ -44,23 +43,3 @@ export const versionSnapshotField: CheckboxField = {
|
||||
},
|
||||
index: true,
|
||||
}
|
||||
|
||||
export function buildLocaleStatusField(config: SanitizedConfig): Field[] {
|
||||
if (!config.localization || !config.localization.locales) {
|
||||
return []
|
||||
}
|
||||
|
||||
return config.localization.locales.map((locale) => {
|
||||
const code = typeof locale === 'string' ? locale : locale.code
|
||||
|
||||
return {
|
||||
name: code,
|
||||
type: 'select',
|
||||
index: true,
|
||||
options: [
|
||||
{ label: ({ t }) => t('version:draft'), value: 'draft' },
|
||||
{ label: ({ t }) => t('version:published'), value: 'published' },
|
||||
],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { SanitizedCollectionConfig } from '../collections/config/types.js'
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { Field, FlattenedField } from '../fields/config/types.js'
|
||||
|
||||
import { buildLocaleStatusField, versionSnapshotField } from './baseFields.js'
|
||||
import { versionSnapshotField } from './baseFields.js'
|
||||
|
||||
export const buildVersionCollectionFields = <T extends boolean = false>(
|
||||
config: SanitizedConfig,
|
||||
@@ -62,23 +62,6 @@ export const buildVersionCollectionFields = <T extends boolean = false>(
|
||||
return locale.code
|
||||
}),
|
||||
})
|
||||
|
||||
if (config.experimental?.localizeStatus) {
|
||||
const localeStatusFields = buildLocaleStatusField(config)
|
||||
|
||||
fields.push({
|
||||
name: 'localeStatus',
|
||||
type: 'group',
|
||||
admin: {
|
||||
disableBulkEdit: true,
|
||||
disabled: true,
|
||||
},
|
||||
fields: localeStatusFields,
|
||||
...(flatten && {
|
||||
flattenedFields: localeStatusFields as FlattenedField[],
|
||||
})!,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fields.push({
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { Field, FlattenedField } from '../fields/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
|
||||
import { buildLocaleStatusField, versionSnapshotField } from './baseFields.js'
|
||||
import { versionSnapshotField } from './baseFields.js'
|
||||
|
||||
export const buildVersionGlobalFields = <T extends boolean = false>(
|
||||
config: SanitizedConfig,
|
||||
@@ -56,23 +56,6 @@ export const buildVersionGlobalFields = <T extends boolean = false>(
|
||||
return locale.code
|
||||
}),
|
||||
})
|
||||
|
||||
if (config.experimental.localizeStatus) {
|
||||
const localeStatusFields = buildLocaleStatusField(config)
|
||||
|
||||
fields.push({
|
||||
name: 'localeStatus',
|
||||
type: 'group',
|
||||
admin: {
|
||||
disableBulkEdit: true,
|
||||
disabled: true,
|
||||
},
|
||||
fields: localeStatusFields,
|
||||
...(flatten && {
|
||||
flattenedFields: localeStatusFields as FlattenedField[],
|
||||
})!,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fields.push({
|
||||
|
||||
@@ -111,12 +111,6 @@ export const replaceWithDraftIfAvailable = async <T extends TypeWithID>({
|
||||
draft.version = {} as T
|
||||
}
|
||||
|
||||
// Lift locale status from version data if available
|
||||
const localeStatus = draft.localeStatus || {}
|
||||
if (locale && localeStatus[locale]) {
|
||||
;(draft.version as { _status?: string })['_status'] = localeStatus[locale]
|
||||
}
|
||||
|
||||
// Disregard all other draft content at this point,
|
||||
// Only interested in the version itself.
|
||||
// Operations will handle firing hooks, etc.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { version } from 'os'
|
||||
|
||||
// @ts-strict-ignore
|
||||
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
|
||||
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
@@ -18,7 +16,6 @@ type Args = {
|
||||
draft?: boolean
|
||||
global?: SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
locale?: null | string
|
||||
operation?: 'create' | 'restoreVersion' | 'update'
|
||||
payload: Payload
|
||||
publishSpecificLocale?: string
|
||||
@@ -34,7 +31,6 @@ export const saveVersion = async ({
|
||||
docWithLocales: doc,
|
||||
draft,
|
||||
global,
|
||||
locale,
|
||||
operation,
|
||||
payload,
|
||||
publishSpecificLocale,
|
||||
@@ -46,7 +42,6 @@ export const saveVersion = async ({
|
||||
let createNewVersion = true
|
||||
const now = new Date().toISOString()
|
||||
const versionData = deepCopyObjectSimple(doc)
|
||||
|
||||
if (draft) {
|
||||
versionData._status = 'draft'
|
||||
}
|
||||
@@ -60,39 +55,39 @@ export const saveVersion = async ({
|
||||
}
|
||||
|
||||
try {
|
||||
let docs
|
||||
const findVersionArgs = {
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
sort: '-updatedAt',
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
;({ docs } = await payload.db.findVersions({
|
||||
...findVersionArgs,
|
||||
collection: collection.slug,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
where: {
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
}))
|
||||
} else {
|
||||
;({ docs } = await payload.db.findGlobalVersions({
|
||||
...findVersionArgs,
|
||||
global: global!.slug,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
}))
|
||||
}
|
||||
const [latestVersion] = docs
|
||||
|
||||
if (autosave) {
|
||||
let docs
|
||||
const findVersionArgs = {
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
sort: '-updatedAt',
|
||||
}
|
||||
|
||||
if (collection) {
|
||||
;({ docs } = await payload.db.findVersions({
|
||||
...findVersionArgs,
|
||||
collection: collection.slug,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
where: {
|
||||
parent: {
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
}))
|
||||
} else {
|
||||
;({ docs } = await payload.db.findGlobalVersions({
|
||||
...findVersionArgs,
|
||||
global: global!.slug,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
}))
|
||||
}
|
||||
const [latestVersion] = docs
|
||||
|
||||
// overwrite the latest version if it's set to autosave
|
||||
if (latestVersion && 'autosave' in latestVersion && latestVersion.autosave === true) {
|
||||
createNewVersion = false
|
||||
@@ -130,53 +125,11 @@ export const saveVersion = async ({
|
||||
}
|
||||
|
||||
if (createNewVersion) {
|
||||
let localeStatus = {}
|
||||
const localizationEnabled =
|
||||
payload.config.localization && payload.config.localization.locales.length > 0
|
||||
|
||||
if (
|
||||
localizationEnabled &&
|
||||
payload.config.localization !== false &&
|
||||
payload.config.experimental?.localizeStatus
|
||||
) {
|
||||
const allLocales = (
|
||||
(payload.config.localization && payload.config.localization?.locales) ||
|
||||
[]
|
||||
).map((locale) => (typeof locale === 'string' ? locale : locale.code))
|
||||
|
||||
// If `publish all`, set all locales to published
|
||||
if (versionData._status === 'published' && !publishSpecificLocale) {
|
||||
localeStatus = Object.fromEntries(allLocales.map((code) => [code, 'published']))
|
||||
} else if (publishSpecificLocale || (locale && versionData._status === 'draft')) {
|
||||
const status: 'draft' | 'published' = publishSpecificLocale ? 'published' : 'draft'
|
||||
const incomingLocale = String(publishSpecificLocale || locale)
|
||||
const existing = latestVersion?.localeStatus
|
||||
|
||||
// If no locale statuses are set, set it and set all others to draft
|
||||
if (!existing) {
|
||||
localeStatus = {
|
||||
...Object.fromEntries(
|
||||
allLocales.filter((code) => code !== incomingLocale).map((code) => [code, 'draft']),
|
||||
),
|
||||
[incomingLocale]: status,
|
||||
}
|
||||
} else {
|
||||
// If locales already exist, update the status for the incoming locale
|
||||
const { [incomingLocale]: _, ...rest } = existing
|
||||
localeStatus = {
|
||||
...rest,
|
||||
[incomingLocale]: status,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createVersionArgs = {
|
||||
autosave: Boolean(autosave),
|
||||
collectionSlug: undefined as string | undefined,
|
||||
createdAt: operation === 'restoreVersion' ? versionData.createdAt : now,
|
||||
globalSlug: undefined as string | undefined,
|
||||
localeStatus,
|
||||
parent: collection ? id : undefined,
|
||||
publishedLocale: publishSpecificLocale || undefined,
|
||||
req,
|
||||
|
||||
@@ -122,7 +122,6 @@ export type SanitizedGlobalVersions = {
|
||||
export type TypeWithVersion<T> = {
|
||||
createdAt: string
|
||||
id: string
|
||||
localeStatus: Record<string, 'draft' | 'published'>
|
||||
parent: number | string
|
||||
publishedLocale?: string
|
||||
snapshot?: boolean
|
||||
|
||||
@@ -65,8 +65,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/find-node-modules": "^2.1.2",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -68,8 +68,8 @@
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/escape-html": "^1.0.4",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"payload": "workspace:*"
|
||||
|
||||
@@ -65,8 +65,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -59,8 +59,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -74,8 +74,8 @@
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/next": "workspace:*",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -72,8 +72,8 @@
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/next": "workspace:*",
|
||||
"@types/lodash.get": "^4.4.7",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/uuid": "10.0.0",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -183,11 +183,6 @@
|
||||
"types": "./src/lexical-proxy/@lexical-react/LexicalContextMenuPlugin.ts",
|
||||
"default": "./src/lexical-proxy/@lexical-react/LexicalContextMenuPlugin.ts"
|
||||
},
|
||||
"./lexical/react/LexicalNodeContextMenuPlugin": {
|
||||
"import": "./src/lexical-proxy/@lexical-react/LexicalNodeContextMenuPlugin.ts",
|
||||
"types": "./src/lexical-proxy/@lexical-react/LexicalNodeContextMenuPlugin.ts",
|
||||
"default": "./src/lexical-proxy/@lexical-react/LexicalNodeContextMenuPlugin.ts"
|
||||
},
|
||||
"./lexical/react/LexicalDecoratorBlockNode": {
|
||||
"import": "./src/lexical-proxy/@lexical-react/LexicalDecoratorBlockNode.ts",
|
||||
"types": "./src/lexical-proxy/@lexical-react/LexicalDecoratorBlockNode.ts",
|
||||
@@ -365,16 +360,16 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@lexical/headless": "0.34.0",
|
||||
"@lexical/html": "0.34.0",
|
||||
"@lexical/link": "0.34.0",
|
||||
"@lexical/list": "0.34.0",
|
||||
"@lexical/mark": "0.34.0",
|
||||
"@lexical/react": "0.34.0",
|
||||
"@lexical/rich-text": "0.34.0",
|
||||
"@lexical/selection": "0.34.0",
|
||||
"@lexical/table": "0.34.0",
|
||||
"@lexical/utils": "0.34.0",
|
||||
"@lexical/headless": "0.28.0",
|
||||
"@lexical/html": "0.28.0",
|
||||
"@lexical/link": "0.28.0",
|
||||
"@lexical/list": "0.28.0",
|
||||
"@lexical/mark": "0.28.0",
|
||||
"@lexical/react": "0.28.0",
|
||||
"@lexical/rich-text": "0.28.0",
|
||||
"@lexical/selection": "0.28.0",
|
||||
"@lexical/table": "0.28.0",
|
||||
"@lexical/utils": "0.28.0",
|
||||
"@payloadcms/translations": "workspace:*",
|
||||
"@payloadcms/ui": "workspace:*",
|
||||
"@types/uuid": "10.0.0",
|
||||
@@ -384,7 +379,7 @@
|
||||
"dequal": "2.0.3",
|
||||
"escape-html": "1.0.3",
|
||||
"jsox": "1.2.121",
|
||||
"lexical": "0.34.0",
|
||||
"lexical": "0.28.0",
|
||||
"mdast-util-from-markdown": "2.0.2",
|
||||
"mdast-util-mdx-jsx": "3.1.3",
|
||||
"micromark-extension-mdx-jsx": "3.0.1",
|
||||
@@ -399,14 +394,14 @@
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"@babel/preset-typescript": "7.27.1",
|
||||
"@lexical/eslint-plugin": "0.34.0",
|
||||
"@lexical/eslint-plugin": "0.28.0",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/escape-html": "1.0.4",
|
||||
"@types/json-schema": "7.0.15",
|
||||
"@types/node": "22.15.30",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
"babel-plugin-transform-remove-imports": "^1.8.0",
|
||||
"esbuild": "0.25.5",
|
||||
"esbuild-sass-plugin": "3.3.1",
|
||||
@@ -586,11 +581,6 @@
|
||||
"types": "./dist/lexical-proxy/@lexical-react/LexicalContextMenuPlugin.d.ts",
|
||||
"default": "./dist/lexical-proxy/@lexical-react/LexicalContextMenuPlugin.js"
|
||||
},
|
||||
"./lexical/react/LexicalNodeContextMenuPlugin": {
|
||||
"import": "./dist/lexical-proxy/@lexical-react/LexicalNodeContextMenuPlugin.js",
|
||||
"types": "./dist/lexical-proxy/@lexical-react/LexicalNodeContextMenuPlugin.d.ts",
|
||||
"default": "./dist/lexical-proxy/@lexical-react/LexicalNodeContextMenuPlugin.js"
|
||||
},
|
||||
"./lexical/react/LexicalDecoratorBlockNode": {
|
||||
"import": "./dist/lexical-proxy/@lexical-react/LexicalDecoratorBlockNode.js",
|
||||
"types": "./dist/lexical-proxy/@lexical-react/LexicalDecoratorBlockNode.d.ts",
|
||||
|
||||
@@ -150,3 +150,6 @@ export { BlockEditButton } from '../../features/blocks/client/component/componen
|
||||
export { BlockRemoveButton } from '../../features/blocks/client/component/components/BlockRemoveButton.js'
|
||||
export { useBlockComponentContext } from '../../features/blocks/client/component/BlockContent.js'
|
||||
export { getRestPopulateFn } from '../../features/converters/utilities/restPopulateFn.js'
|
||||
|
||||
export { RenderLexical } from '../../field/RenderLexical/index.js'
|
||||
export { buildEditorState } from '../../utilities/buildEditorState.js'
|
||||
|
||||
@@ -8,18 +8,17 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
||||
import { useLexicalEditable } from '@lexical/react/useLexicalEditable'
|
||||
import {
|
||||
$computeTableMapSkipCellCheck,
|
||||
$deleteTableColumnAtSelection,
|
||||
$deleteTableRowAtSelection,
|
||||
$deleteTableColumn__EXPERIMENTAL,
|
||||
$deleteTableRow__EXPERIMENTAL,
|
||||
$getNodeTriplet,
|
||||
$getTableCellNodeFromLexicalNode,
|
||||
$getTableColumnIndexFromTableCellNode,
|
||||
$getTableNodeFromLexicalNodeOrThrow,
|
||||
$getTableRowIndexFromTableCellNode,
|
||||
$insertTableColumnAtSelection,
|
||||
$insertTableRowAtSelection,
|
||||
$insertTableColumn__EXPERIMENTAL,
|
||||
$insertTableRow__EXPERIMENTAL,
|
||||
$isTableCellNode,
|
||||
$isTableSelection,
|
||||
$mergeCells,
|
||||
$unmergeCell,
|
||||
getTableElement,
|
||||
getTableObserverFromTableElement,
|
||||
@@ -29,8 +28,11 @@ import {
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { useScrollInfo } from '@payloadcms/ui'
|
||||
import {
|
||||
$createParagraphNode,
|
||||
$getRoot,
|
||||
$getSelection,
|
||||
$isElementNode,
|
||||
$isParagraphNode,
|
||||
$isRangeSelection,
|
||||
$isTextNode,
|
||||
$setSelection,
|
||||
@@ -72,6 +74,17 @@ function $canUnmerge(): boolean {
|
||||
return cell.__colSpan > 1 || cell.__rowSpan > 1
|
||||
}
|
||||
|
||||
function $cellContainsEmptyParagraph(cell: TableCellNode): boolean {
|
||||
if (cell.getChildrenSize() !== 1) {
|
||||
return false
|
||||
}
|
||||
const firstChild = cell.getFirstChildOrThrow()
|
||||
if (!$isParagraphNode(firstChild) || !firstChild.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function $selectLastDescendant(node: ElementNode): void {
|
||||
const lastDescendant = node.getLastDescendant()
|
||||
if ($isTextNode(lastDescendant)) {
|
||||
@@ -214,14 +227,105 @@ function TableActionMenu({
|
||||
const mergeTableCellsAtSelection = () => {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection()
|
||||
if (!$isTableSelection(selection)) {
|
||||
return
|
||||
}
|
||||
const nodes = selection.getNodes()
|
||||
const tableCells = nodes.filter($isTableCellNode)
|
||||
const targetCell = $mergeCells(tableCells)
|
||||
if ($isTableSelection(selection)) {
|
||||
// Get all selected cells and compute the total area
|
||||
const nodes = selection.getNodes()
|
||||
const tableCells = nodes.filter($isTableCellNode)
|
||||
|
||||
if (tableCells.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find the table node
|
||||
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCells[0] as TableCellNode)
|
||||
const [gridMap] = $computeTableMapSkipCellCheck(tableNode, null, null)
|
||||
|
||||
// Find the boundaries of the selection including merged cells
|
||||
let minRow = Infinity
|
||||
let maxRow = -Infinity
|
||||
let minCol = Infinity
|
||||
let maxCol = -Infinity
|
||||
|
||||
// First pass: find the actual boundaries considering merged cells
|
||||
const processedCells = new Set()
|
||||
for (const row of gridMap) {
|
||||
for (const mapCell of row) {
|
||||
if (!mapCell || !mapCell.cell) {
|
||||
continue
|
||||
}
|
||||
|
||||
const cellKey = mapCell.cell.getKey()
|
||||
if (processedCells.has(cellKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (tableCells.some((cell) => cell.is(mapCell.cell))) {
|
||||
processedCells.add(cellKey)
|
||||
// Get the actual position of this cell in the grid
|
||||
const cellStartRow = mapCell.startRow
|
||||
const cellStartCol = mapCell.startColumn
|
||||
const cellRowSpan = mapCell.cell.__rowSpan || 1
|
||||
const cellColSpan = mapCell.cell.__colSpan || 1
|
||||
|
||||
// Update boundaries considering the cell's actual position and span
|
||||
minRow = Math.min(minRow, cellStartRow)
|
||||
maxRow = Math.max(maxRow, cellStartRow + cellRowSpan - 1)
|
||||
minCol = Math.min(minCol, cellStartCol)
|
||||
maxCol = Math.max(maxCol, cellStartCol + cellColSpan - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate boundaries
|
||||
if (minRow === Infinity || minCol === Infinity) {
|
||||
return
|
||||
}
|
||||
|
||||
// The total span of the merged cell
|
||||
const totalRowSpan = maxRow - minRow + 1
|
||||
const totalColSpan = maxCol - minCol + 1
|
||||
|
||||
// Use the top-left cell as the target cell
|
||||
const targetCellMap = gridMap?.[minRow]?.[minCol]
|
||||
if (!targetCellMap?.cell) {
|
||||
return
|
||||
}
|
||||
const targetCell = targetCellMap.cell
|
||||
|
||||
// Set the spans for the target cell
|
||||
targetCell.setColSpan(totalColSpan)
|
||||
targetCell.setRowSpan(totalRowSpan)
|
||||
|
||||
// Move content from other cells to the target cell
|
||||
const seenCells = new Set([targetCell.getKey()])
|
||||
|
||||
// Second pass: merge content and remove other cells
|
||||
for (let row = minRow; row <= maxRow; row++) {
|
||||
for (let col = minCol; col <= maxCol; col++) {
|
||||
const mapCell = gridMap?.[row]?.[col]
|
||||
if (!mapCell?.cell) {
|
||||
continue
|
||||
}
|
||||
|
||||
const currentCell = mapCell.cell
|
||||
const key = currentCell.getKey()
|
||||
|
||||
if (!seenCells.has(key)) {
|
||||
seenCells.add(key)
|
||||
const isEmpty = $cellContainsEmptyParagraph(currentCell)
|
||||
if (!isEmpty) {
|
||||
targetCell.append(...currentCell.getChildren())
|
||||
}
|
||||
currentCell.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure target cell has content
|
||||
if (targetCell.getChildrenSize() === 0) {
|
||||
targetCell.append($createParagraphNode())
|
||||
}
|
||||
|
||||
if (targetCell) {
|
||||
$selectLastDescendant(targetCell)
|
||||
onClose()
|
||||
}
|
||||
@@ -238,7 +342,7 @@ function TableActionMenu({
|
||||
(shouldInsertAfter: boolean) => {
|
||||
editor.update(() => {
|
||||
for (let i = 0; i < selectionCounts.rows; i++) {
|
||||
$insertTableRowAtSelection(shouldInsertAfter)
|
||||
$insertTableRow__EXPERIMENTAL(shouldInsertAfter)
|
||||
}
|
||||
onClose()
|
||||
})
|
||||
@@ -250,7 +354,7 @@ function TableActionMenu({
|
||||
(shouldInsertAfter: boolean) => {
|
||||
editor.update(() => {
|
||||
for (let i = 0; i < selectionCounts.columns; i++) {
|
||||
$insertTableColumnAtSelection(shouldInsertAfter)
|
||||
$insertTableColumn__EXPERIMENTAL(shouldInsertAfter)
|
||||
}
|
||||
onClose()
|
||||
})
|
||||
@@ -260,7 +364,7 @@ function TableActionMenu({
|
||||
|
||||
const deleteTableRowAtSelection = useCallback(() => {
|
||||
editor.update(() => {
|
||||
$deleteTableRowAtSelection()
|
||||
$deleteTableRow__EXPERIMENTAL()
|
||||
onClose()
|
||||
})
|
||||
}, [editor, onClose])
|
||||
@@ -277,7 +381,7 @@ function TableActionMenu({
|
||||
|
||||
const deleteTableColumnAtSelection = useCallback(() => {
|
||||
editor.update(() => {
|
||||
$deleteTableColumnAtSelection()
|
||||
$deleteTableColumn__EXPERIMENTAL()
|
||||
onClose()
|
||||
})
|
||||
}, [editor, onClose])
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
TableNode,
|
||||
} from '@lexical/table'
|
||||
import { calculateZoomLevel, mergeRegister } from '@lexical/utils'
|
||||
import { $getNearestNodeFromDOMNode, isHTMLElement, SKIP_SCROLL_INTO_VIEW_TAG } from 'lexical'
|
||||
import { $getNearestNodeFromDOMNode, isHTMLElement } from 'lexical'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
@@ -223,7 +223,7 @@ function TableCellResizer({ editor }: { editor: LexicalEditor }): JSX.Element {
|
||||
const newHeight = Math.max(height + heightChange, MIN_ROW_HEIGHT)
|
||||
tableRow.setHeight(newHeight)
|
||||
},
|
||||
{ tag: SKIP_SCROLL_INTO_VIEW_TAG },
|
||||
{ tag: 'skip-scroll-into-view' },
|
||||
)
|
||||
},
|
||||
[activeCell, editor],
|
||||
@@ -281,7 +281,7 @@ function TableCellResizer({ editor }: { editor: LexicalEditor }): JSX.Element {
|
||||
newColWidths[columnIndex] = newWidth
|
||||
tableNode.setColWidths(newColWidths)
|
||||
},
|
||||
{ tag: SKIP_SCROLL_INTO_VIEW_TAG },
|
||||
{ tag: 'skip-scroll-into-view' },
|
||||
)
|
||||
},
|
||||
[activeCell, editor],
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
$getTableAndElementByKey,
|
||||
$getTableColumnIndexFromTableCellNode,
|
||||
$getTableRowIndexFromTableCellNode,
|
||||
$insertTableColumnAtSelection,
|
||||
$insertTableRowAtSelection,
|
||||
$insertTableColumn__EXPERIMENTAL,
|
||||
$insertTableRow__EXPERIMENTAL,
|
||||
$isTableCellNode,
|
||||
$isTableNode,
|
||||
getTableElement,
|
||||
@@ -223,10 +223,10 @@ function TableHoverActionsContainer({
|
||||
const maybeTableNode = $getNearestNodeFromDOMNode(tableCellDOMNodeRef.current)
|
||||
maybeTableNode?.selectEnd()
|
||||
if (insertRow) {
|
||||
$insertTableRowAtSelection()
|
||||
$insertTableRow__EXPERIMENTAL()
|
||||
setShownRow(false)
|
||||
} else {
|
||||
$insertTableColumnAtSelection()
|
||||
$insertTableColumn__EXPERIMENTAL()
|
||||
setShownColumn(false)
|
||||
}
|
||||
}
|
||||
|
||||
115
packages/richtext-lexical/src/field/RenderLexical/index.tsx
Normal file
115
packages/richtext-lexical/src/field/RenderLexical/index.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
'use client'
|
||||
import {
|
||||
FieldContext,
|
||||
FieldPathContext,
|
||||
type FieldType,
|
||||
type RenderFieldServerFnArgs,
|
||||
ServerFunctionsContext,
|
||||
type ServerFunctionsContextType,
|
||||
ShimmerEffect,
|
||||
useServerFunctions,
|
||||
} from '@payloadcms/ui'
|
||||
import React, { useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
import type { DefaultTypedEditorState } from '../../nodeTypes.js'
|
||||
|
||||
/**
|
||||
* Utility to render a lexical editor on the client.
|
||||
*
|
||||
* @experimental - may break in minor releases
|
||||
* @todo - replace this with a general utility that works for all fields. Maybe merge with packages/ui/src/forms/RenderFields/RenderField.tsx
|
||||
*/
|
||||
export const RenderLexical: React.FC<
|
||||
/**
|
||||
* If value or setValue, or both, is provided, this component will manage its own value.
|
||||
* If neither is passed, it will rely on the parent form to manage the value.
|
||||
*/
|
||||
{
|
||||
setValue?: FieldType<DefaultTypedEditorState | undefined>['setValue']
|
||||
|
||||
value?: FieldType<DefaultTypedEditorState | undefined>['value']
|
||||
} & RenderFieldServerFnArgs
|
||||
> = (args) => {
|
||||
const { field, initialValue, path, schemaPath, setValue, value } = args
|
||||
const [Component, setComponent] = React.useState<null | React.ReactNode>(null)
|
||||
const serverFunctionContext = useServerFunctions()
|
||||
const { _internal_renderField } = serverFunctionContext
|
||||
|
||||
const [entityType, entitySlug] = schemaPath.split('.')
|
||||
|
||||
const fieldPath = path ?? (field && 'name' in field ? field?.name : '') ?? ''
|
||||
|
||||
const renderLexical = useCallback(() => {
|
||||
async function render() {
|
||||
const { Field } = await _internal_renderField({
|
||||
field,
|
||||
initialValue: initialValue ?? undefined,
|
||||
path,
|
||||
schemaPath,
|
||||
})
|
||||
|
||||
setComponent(Field)
|
||||
}
|
||||
void render()
|
||||
}, [_internal_renderField, schemaPath, path, field, initialValue])
|
||||
|
||||
const mounted = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (mounted.current) {
|
||||
return
|
||||
}
|
||||
mounted.current = true
|
||||
void renderLexical()
|
||||
}, [renderLexical])
|
||||
|
||||
if (!Component) {
|
||||
return <ShimmerEffect />
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, the lexical will make form state requests (e.g. to get drawer fields), passing in the arguments
|
||||
* of the current field. However, we need to override those arguments to get it to make requests based on the
|
||||
* *target* field. The server only knows the schema map of the target field.
|
||||
*/
|
||||
const adjustedServerFunctionContext: ServerFunctionsContextType = {
|
||||
...serverFunctionContext,
|
||||
getFormState: async (getFormStateArgs) => {
|
||||
return serverFunctionContext.getFormState({
|
||||
...getFormStateArgs,
|
||||
collectionSlug: entityType === 'collection' ? entitySlug : undefined,
|
||||
globalSlug: entityType === 'global' ? entitySlug : undefined,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined' && !setValue) {
|
||||
return (
|
||||
<ServerFunctionsContext value={{ ...adjustedServerFunctionContext }}>
|
||||
<FieldPathContext key={fieldPath} value={fieldPath}>
|
||||
{Component}
|
||||
</FieldPathContext>
|
||||
</ServerFunctionsContext>
|
||||
)
|
||||
}
|
||||
|
||||
const fieldValue: FieldType<DefaultTypedEditorState | undefined> = {
|
||||
disabled: false,
|
||||
formInitializing: false,
|
||||
formProcessing: false,
|
||||
formSubmitted: false,
|
||||
initialValue: value,
|
||||
path: fieldPath,
|
||||
setValue: setValue ?? (() => undefined),
|
||||
showError: false,
|
||||
value,
|
||||
}
|
||||
|
||||
return (
|
||||
<ServerFunctionsContext value={{ ...adjustedServerFunctionContext }}>
|
||||
<FieldPathContext key={fieldPath} value={fieldPath}>
|
||||
<FieldContext value={fieldValue}>{Component}</FieldContext>
|
||||
</FieldPathContext>
|
||||
</ServerFunctionsContext>
|
||||
)
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import { richTextValidateHOC } from './validate/index.js'
|
||||
|
||||
let checkedDependencies = false
|
||||
|
||||
export const lexicalTargetVersion = '0.34.0'
|
||||
export const lexicalTargetVersion = '0.28.0'
|
||||
|
||||
export function lexicalEditor(args?: LexicalEditorProps): LexicalRichTextAdapterProvider {
|
||||
if (
|
||||
@@ -1055,10 +1055,12 @@ export { populate } from './populateGraphQL/populate.js'
|
||||
|
||||
export type { LexicalEditorProps, LexicalFieldAdminProps, LexicalRichTextAdapter } from './types.js'
|
||||
|
||||
export { buildEditorState } from './utilities/buildEditorState.js'
|
||||
export { createServerFeature } from './utilities/createServerFeature.js'
|
||||
export { editorConfigFactory } from './utilities/editorConfigFactory.js'
|
||||
|
||||
export { editorConfigFactory } from './utilities/editorConfigFactory.js'
|
||||
export type { FieldsDrawerProps } from './utilities/fieldsDrawer/Drawer.js'
|
||||
|
||||
export { extractPropsFromJSXPropsString } from './utilities/jsx/extractPropsFromJSXPropsString.js'
|
||||
|
||||
export {
|
||||
@@ -1067,5 +1069,4 @@ export {
|
||||
objectToFrontmatter,
|
||||
propsToJSXString,
|
||||
} from './utilities/jsx/jsx.js'
|
||||
|
||||
export { upgradeLexicalData } from './utilities/upgradeLexicalData/index.js'
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@lexical/react/LexicalNodeContextMenuPlugin'
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
@layer payload-default {
|
||||
.draggable-block-menu {
|
||||
border: none;
|
||||
border-radius: $style-radius-m;
|
||||
padding: 0;
|
||||
cursor: grab;
|
||||
@@ -25,7 +24,6 @@
|
||||
}
|
||||
|
||||
.icon {
|
||||
background-color: transparent;
|
||||
width: 18px;
|
||||
height: 24px;
|
||||
opacity: 0.3;
|
||||
|
||||
@@ -70,7 +70,7 @@ function useDraggableBlockMenu(
|
||||
): React.ReactElement {
|
||||
const scrollerElem = anchorElem.parentElement
|
||||
|
||||
const menuRef = useRef<HTMLButtonElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const targetLineRef = useRef<HTMLDivElement>(null)
|
||||
const debugHighlightRef = useRef<HTMLDivElement>(null)
|
||||
const isDraggingBlockRef = useRef<boolean>(false)
|
||||
@@ -396,7 +396,7 @@ function useDraggableBlockMenu(
|
||||
editorConfig?.admin?.hideGutter,
|
||||
])
|
||||
|
||||
function onDragStart(event: ReactDragEvent<HTMLButtonElement>): void {
|
||||
function onDragStart(event: ReactDragEvent<HTMLDivElement>): void {
|
||||
const dataTransfer = event.dataTransfer
|
||||
if (!dataTransfer || !draggableBlockElem) {
|
||||
return
|
||||
@@ -422,17 +422,15 @@ function useDraggableBlockMenu(
|
||||
|
||||
return createPortal(
|
||||
<React.Fragment>
|
||||
<button
|
||||
aria-label="Drag to move"
|
||||
<div
|
||||
className="icon draggable-block-menu"
|
||||
draggable
|
||||
onDragEnd={onDragEnd}
|
||||
onDragStart={onDragStart}
|
||||
ref={menuRef}
|
||||
type="button"
|
||||
>
|
||||
<div className={isEditable ? 'icon' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="draggable-block-target-line" ref={targetLineRef} />
|
||||
<div className="debug-highlight" ref={debugHighlightRef} />
|
||||
</React.Fragment>,
|
||||
|
||||
@@ -78,9 +78,17 @@ type RecursiveNodes<T extends SerializedLexicalNode, Depth extends number = 4> =
|
||||
|
||||
type DecrementDepth<N extends number> = [0, 0, 1, 2, 3, 4][N]
|
||||
|
||||
/**
|
||||
* Alternative type to `SerializedEditorState` that automatically types your nodes
|
||||
* more strictly, narrowing down nodes based on the `type` without having to manually
|
||||
* type-cast.
|
||||
*/
|
||||
export type TypedEditorState<T extends SerializedLexicalNode = SerializedLexicalNode> =
|
||||
SerializedEditorState<RecursiveNodes<T>>
|
||||
|
||||
/**
|
||||
* All node types included by default in a lexical editor without configuration.
|
||||
*/
|
||||
export type DefaultNodeTypes =
|
||||
| SerializedAutoLinkNode
|
||||
//| SerializedBlockNode // Not included by default
|
||||
@@ -97,5 +105,12 @@ export type DefaultNodeTypes =
|
||||
| SerializedTextNode
|
||||
| SerializedUploadNode
|
||||
|
||||
export type DefaultTypedEditorState<T extends SerializedLexicalNode = SerializedLexicalNode> =
|
||||
TypedEditorState<DefaultNodeTypes | T>
|
||||
/**
|
||||
* Like `TypedEditorState` but includes all default node types.
|
||||
* You can pass *additional* node types as a generic parameter.
|
||||
*/
|
||||
export type DefaultTypedEditorState<
|
||||
TAdditionalNodeTypes extends null | SerializedLexicalNode = null,
|
||||
> = [TAdditionalNodeTypes] extends null
|
||||
? TypedEditorState<DefaultNodeTypes>
|
||||
: TypedEditorState<DefaultNodeTypes | NonNullable<TAdditionalNodeTypes>>
|
||||
|
||||
99
packages/richtext-lexical/src/utilities/buildEditorState.ts
Normal file
99
packages/richtext-lexical/src/utilities/buildEditorState.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { SerializedLexicalNode } from 'lexical'
|
||||
|
||||
import type { DefaultTypedEditorState, TypedEditorState } from '../nodeTypes.js'
|
||||
|
||||
export function buildEditorState(args: {
|
||||
nodes?: DefaultTypedEditorState['root']['children']
|
||||
text?: string
|
||||
}): DefaultTypedEditorState
|
||||
|
||||
export function buildEditorState<T extends SerializedLexicalNode>(args: {
|
||||
// If you pass children typed for a specific schema T, the return is TypedEditorState<T>
|
||||
nodes?: TypedEditorState<T>['root']['children']
|
||||
text?: string
|
||||
}): TypedEditorState<T>
|
||||
|
||||
/**
|
||||
* Helper to build lexical editor state JSON from text and/or nodes.
|
||||
*
|
||||
* @param nodes - The nodes to include in the editor state. If you pass the `text` argument, this will append your nodes after the first paragraph node.
|
||||
* @param text - The text content to include in the editor state. This will create a paragraph node with a text node for you and set that as the first node.
|
||||
* @returns The constructed editor state JSON.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* just passing text:
|
||||
*
|
||||
* ```ts
|
||||
* const editorState = buildEditorState({ text: 'Hello world' }) // result typed as DefaultTypedEditorState
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* passing nodes:
|
||||
*
|
||||
* ```ts
|
||||
* const editorState = // result typed as TypedEditorState<DefaultNodeTypes | SerializedBlockNode> (or TypedEditorState<SerializedBlockNode>)
|
||||
* buildEditorState<DefaultNodeTypes | SerializedBlockNode>({ // or just buildEditorState<SerializedBlockNode> if you *only* want to allow block nodes
|
||||
* nodes: [
|
||||
* {
|
||||
* type: 'block',
|
||||
* fields: {
|
||||
* id: 'id',
|
||||
* blockName: 'Cool block',
|
||||
* blockType: 'myBlock',
|
||||
* },
|
||||
* format: 'left',
|
||||
* version: 1,
|
||||
* }
|
||||
* ],
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function buildEditorState<T extends SerializedLexicalNode>({
|
||||
nodes,
|
||||
text,
|
||||
}: {
|
||||
nodes?: DefaultTypedEditorState['root']['children'] | TypedEditorState<T>['root']['children']
|
||||
text?: string
|
||||
}): DefaultTypedEditorState | TypedEditorState<T> {
|
||||
const editorJSON: DefaultTypedEditorState = {
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
}
|
||||
|
||||
if (text) {
|
||||
editorJSON.root.children.push({
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
detail: 0,
|
||||
format: 0,
|
||||
mode: 'normal',
|
||||
style: '',
|
||||
text,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: 'ltr',
|
||||
format: '',
|
||||
indent: 0,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
version: 1,
|
||||
})
|
||||
}
|
||||
|
||||
if (nodes?.length) {
|
||||
editorJSON.root.children.push(...(nodes as any))
|
||||
}
|
||||
|
||||
return editorJSON
|
||||
}
|
||||
@@ -67,8 +67,8 @@
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/is-hotkey": "^0.1.10",
|
||||
"@types/node": "22.15.30",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"payload": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@swc/core": "1.11.29",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"dotenv": "16.4.7",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "5.7.3"
|
||||
|
||||
@@ -168,10 +168,10 @@
|
||||
"@babel/preset-typescript": "7.27.1",
|
||||
"@hyrious/esbuild-plugin-commonjs": "0.2.6",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react": "19.1.12",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/uuid": "10.0.0",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
"esbuild": "0.25.5",
|
||||
"esbuild-sass-plugin": "3.3.1",
|
||||
"payload": "workspace:*"
|
||||
|
||||
@@ -134,13 +134,13 @@ export const Autosave: React.FC<Props> = ({ id, collection, global: globalDoc })
|
||||
|
||||
if (collection && id) {
|
||||
entitySlug = collection.slug
|
||||
url = `${serverURL}${api}/${entitySlug}/${id}?depth=0&draft=true&autosave=true&locale=${localeRef.current}`
|
||||
url = `${serverURL}${api}/${entitySlug}/${id}?draft=true&autosave=true&locale=${localeRef.current}`
|
||||
method = 'PATCH'
|
||||
}
|
||||
|
||||
if (globalDoc) {
|
||||
entitySlug = globalDoc.slug
|
||||
url = `${serverURL}${api}/globals/${entitySlug}?depth=0&draft=true&autosave=true&locale=${localeRef.current}`
|
||||
url = `${serverURL}${api}/globals/${entitySlug}?draft=true&autosave=true&locale=${localeRef.current}`
|
||||
method = 'POST'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
'use client'
|
||||
import type { ClientComponentProps } from 'payload'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import { useIntersect } from '../../hooks/useIntersect.js'
|
||||
|
||||
export const RenderIfInViewport: React.FC<{
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
forceRender?: boolean
|
||||
}> = ({ children, className, forceRender }) => {
|
||||
export const RenderIfInViewport: React.FC<
|
||||
{
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
} & Pick<ClientComponentProps, 'forceRender'>
|
||||
> = ({ children, className, forceRender }) => {
|
||||
const [hasRendered, setHasRendered] = React.useState(Boolean(forceRender))
|
||||
const [intersectionRef, entry] = useIntersect(
|
||||
{
|
||||
|
||||
@@ -24,7 +24,6 @@ export const Status: React.FC = () => {
|
||||
hasPublishedDoc,
|
||||
incrementVersionCount,
|
||||
isTrashed,
|
||||
savedDocumentData: doc,
|
||||
setHasPublishedDoc,
|
||||
setMostRecentVersionIsAutosaved,
|
||||
setUnpublishedVersionCount,
|
||||
@@ -38,7 +37,6 @@ export const Status: React.FC = () => {
|
||||
routes: { api },
|
||||
serverURL,
|
||||
},
|
||||
getEntityConfig,
|
||||
} = useConfig()
|
||||
|
||||
const { reset: resetForm } = useForm()
|
||||
@@ -48,22 +46,16 @@ export const Status: React.FC = () => {
|
||||
const unPublishModalSlug = `confirm-un-publish-${id}`
|
||||
const revertModalSlug = `confirm-revert-${id}`
|
||||
|
||||
let statusToRender: 'changed' | 'draft' | 'published' = 'draft'
|
||||
let statusToRender: 'changed' | 'draft' | 'published'
|
||||
|
||||
const collectionConfig = getEntityConfig({ collectionSlug })
|
||||
const globalConfig = getEntityConfig({ globalSlug })
|
||||
|
||||
const docConfig = collectionConfig || globalConfig
|
||||
const autosaveEnabled =
|
||||
typeof docConfig?.versions?.drafts === 'object' ? docConfig.versions.drafts.autosave : false
|
||||
|
||||
if (autosaveEnabled) {
|
||||
if (hasPublishedDoc) {
|
||||
statusToRender = unpublishedVersionCount > 0 ? 'changed' : 'published'
|
||||
}
|
||||
} else {
|
||||
statusToRender = doc._status || 'draft'
|
||||
if (unpublishedVersionCount > 0 && hasPublishedDoc) {
|
||||
statusToRender = 'changed'
|
||||
} else if (!hasPublishedDoc) {
|
||||
statusToRender = 'draft'
|
||||
} else if (hasPublishedDoc && unpublishedVersionCount <= 0) {
|
||||
statusToRender = 'published'
|
||||
}
|
||||
|
||||
const displayStatusKey = isTrashed
|
||||
? hasPublishedDoc
|
||||
? 'previouslyPublished'
|
||||
@@ -198,7 +190,7 @@ export const Status: React.FC = () => {
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{!isTrashed && canUpdate && statusToRender === 'changed' || statusToRender === 'draft' && (
|
||||
{!isTrashed && canUpdate && statusToRender === 'changed' && (
|
||||
<React.Fragment>
|
||||
—
|
||||
<Button
|
||||
|
||||
@@ -235,12 +235,14 @@ export type { FieldAction } from '../../forms/Form/types.js'
|
||||
export { fieldReducer } from '../../forms/Form/fieldReducer.js'
|
||||
export { NullifyLocaleField } from '../../forms/NullifyField/index.js'
|
||||
export { RenderFields } from '../../forms/RenderFields/index.js'
|
||||
export { FieldPathContext, useFieldPath } from '../../forms/RenderFields/context.js'
|
||||
|
||||
export { RowLabel, type RowLabelProps } from '../../forms/RowLabel/index.js'
|
||||
export { RowLabelProvider, useRowLabel } from '../../forms/RowLabel/Context/index.js'
|
||||
|
||||
export { FormSubmit } from '../../forms/Submit/index.js'
|
||||
export { WatchChildErrors } from '../../forms/WatchChildErrors/index.js'
|
||||
export { useField } from '../../forms/useField/index.js'
|
||||
export { FieldContext, useField } from '../../forms/useField/index.js'
|
||||
export type { FieldType, Options } from '../../forms/useField/types.js'
|
||||
|
||||
export { withCondition } from '../../forms/withCondition/index.js'
|
||||
@@ -287,6 +289,8 @@ export { Warning as WarningIcon } from '../../providers/ToastContainer/icons/War
|
||||
export {
|
||||
type RenderDocumentResult,
|
||||
type RenderDocumentServerFunction,
|
||||
ServerFunctionsContext,
|
||||
type ServerFunctionsContextType,
|
||||
ServerFunctionsProvider,
|
||||
useServerFunctions,
|
||||
} from '../../providers/ServerFunctions/index.js'
|
||||
@@ -407,3 +411,7 @@ export { parseSearchParams } from '../../utilities/parseSearchParams.js'
|
||||
export { FieldDiffLabel } from '../../elements/FieldDiffLabel/index.js'
|
||||
export { FieldDiffContainer } from '../../elements/FieldDiffContainer/index.js'
|
||||
export { formatTimeToNow } from '../../utilities/formatDocTitle/formatDateTitle.js'
|
||||
export type {
|
||||
RenderFieldServerFnArgs,
|
||||
RenderFieldServerFnReturnType,
|
||||
} from '../../forms/fieldSchemasToFormState/serverFunctions/renderFieldServerFn.js'
|
||||
|
||||
@@ -3,6 +3,7 @@ export { FieldDiffLabel } from '../../elements/FieldDiffLabel/index.js'
|
||||
export { FolderTableCell } from '../../elements/FolderView/Cell/index.server.js'
|
||||
export { FolderField } from '../../elements/FolderView/FolderField/index.server.js'
|
||||
export { getHTMLDiffComponents } from '../../elements/HTMLDiff/index.js'
|
||||
export { _internal_renderFieldHandler } from '../../forms/fieldSchemasToFormState/serverFunctions/renderFieldServerFn.js'
|
||||
export { File } from '../../graphics/File/index.js'
|
||||
export { CheckIcon } from '../../icons/Check/index.js'
|
||||
export { copyDataFromLocaleHandler } from '../../utilities/copyDataFromLocale.js'
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
'use client'
|
||||
import type { ArrayField, ClientField, Row, SanitizedFieldPermissions } from 'payload'
|
||||
import type {
|
||||
ArrayField,
|
||||
ClientComponentProps,
|
||||
ClientField,
|
||||
Row,
|
||||
SanitizedFieldPermissions,
|
||||
} from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
@@ -26,7 +32,6 @@ type ArrayRowProps = {
|
||||
readonly duplicateRow: (rowIndex: number) => void
|
||||
readonly errorCount: number
|
||||
readonly fields: ClientField[]
|
||||
readonly forceRender?: boolean
|
||||
readonly hasMaxRows?: boolean
|
||||
readonly isLoading?: boolean
|
||||
readonly isSortable?: boolean
|
||||
@@ -43,7 +48,8 @@ type ArrayRowProps = {
|
||||
readonly rowIndex: number
|
||||
readonly schemaPath: string
|
||||
readonly setCollapse: (rowID: string, collapsed: boolean) => void
|
||||
} & UseDraggableSortableReturn
|
||||
} & Pick<ClientComponentProps, 'forceRender'> &
|
||||
UseDraggableSortableReturn
|
||||
|
||||
export const ArrayRow: React.FC<ArrayRowProps> = ({
|
||||
addRow,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
import type {
|
||||
ClientComponentProps,
|
||||
ClientField,
|
||||
ClientTab,
|
||||
DocumentPreferences,
|
||||
@@ -249,7 +250,6 @@ export const TabsField = withCondition(TabsFieldComponent)
|
||||
type ActiveTabProps = {
|
||||
readonly description: StaticDescription
|
||||
readonly fields: ClientField[]
|
||||
readonly forceRender?: boolean
|
||||
readonly hidden: boolean
|
||||
readonly label?: string
|
||||
readonly parentIndexPath: string
|
||||
@@ -258,7 +258,7 @@ type ActiveTabProps = {
|
||||
readonly path: string
|
||||
readonly permissions: SanitizedFieldPermissions
|
||||
readonly readOnly: boolean
|
||||
}
|
||||
} & Pick<ClientComponentProps, 'forceRender'>
|
||||
|
||||
function TabContent({
|
||||
description,
|
||||
|
||||
@@ -134,37 +134,35 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
|
||||
case 'DUPLICATE_ROW': {
|
||||
const { path, rowIndex } = action
|
||||
const { remainingFields, rows } = separateRows(path, state)
|
||||
const rowsWithDuplicate = [...(state[path].rows || [])]
|
||||
const rowsMetadata = [...(state[path].rows || [])]
|
||||
|
||||
const newRow = deepCopyObjectSimpleWithoutReactComponents(rowsWithDuplicate[rowIndex])
|
||||
const duplicateRowMetadata = deepCopyObjectSimpleWithoutReactComponents(
|
||||
rowsMetadata[rowIndex],
|
||||
)
|
||||
|
||||
const newRowID = new ObjectId().toHexString()
|
||||
|
||||
if (newRow.id) {
|
||||
newRow.id = newRowID
|
||||
if (duplicateRowMetadata.id) {
|
||||
duplicateRowMetadata.id = new ObjectId().toHexString()
|
||||
}
|
||||
|
||||
if (rowsWithDuplicate[rowIndex]?.customComponents?.RowLabel) {
|
||||
newRow.customComponents = {
|
||||
RowLabel: rowsWithDuplicate[rowIndex].customComponents.RowLabel,
|
||||
if (rowsMetadata[rowIndex]?.customComponents?.RowLabel) {
|
||||
duplicateRowMetadata.customComponents = {
|
||||
RowLabel: rowsMetadata[rowIndex].customComponents.RowLabel,
|
||||
}
|
||||
}
|
||||
|
||||
const duplicateRowState = deepCopyObjectSimpleWithoutReactComponents(rows[rowIndex])
|
||||
|
||||
if (duplicateRowState.id) {
|
||||
duplicateRowState.id.value = newRowID
|
||||
duplicateRowState.id.initialValue = newRowID
|
||||
duplicateRowState.id.value = new ObjectId().toHexString()
|
||||
duplicateRowState.id.initialValue = new ObjectId().toHexString()
|
||||
}
|
||||
|
||||
for (const key of Object.keys(duplicateRowState).filter((key) => key.endsWith('.id'))) {
|
||||
const idState = duplicateRowState[key]
|
||||
|
||||
const newNestedFieldID = new ObjectId().toHexString()
|
||||
|
||||
if (idState && typeof idState.value === 'string' && ObjectId.isValid(idState.value)) {
|
||||
duplicateRowState[key].value = newNestedFieldID
|
||||
duplicateRowState[key].initialValue = newNestedFieldID
|
||||
duplicateRowState[key].value = new ObjectId().toHexString()
|
||||
duplicateRowState[key].initialValue = new ObjectId().toHexString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +170,7 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
|
||||
if (Object.keys(duplicateRowState).length > 0) {
|
||||
// Add new object containing subfield names to unflattenedRows array
|
||||
rows.splice(rowIndex + 1, 0, duplicateRowState)
|
||||
rowsWithDuplicate.splice(rowIndex + 1, 0, newRow)
|
||||
rowsMetadata.splice(rowIndex + 1, 0, duplicateRowMetadata)
|
||||
}
|
||||
|
||||
const newState = {
|
||||
@@ -181,7 +179,7 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
|
||||
[path]: {
|
||||
...state[path],
|
||||
disableFormData: true,
|
||||
rows: rowsWithDuplicate,
|
||||
rows: rowsMetadata,
|
||||
value: rows.length,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import type { ClientField, SanitizedFieldPermissions } from 'payload'
|
||||
import type { ClientComponentProps, ClientField, SanitizedFieldPermissions } from 'payload'
|
||||
|
||||
export type RenderFieldsProps = {
|
||||
readonly className?: string
|
||||
readonly fields: ClientField[]
|
||||
/**
|
||||
* Controls the rendering behavior of the fields, i.e. defers rendering until they intersect with the viewport using the Intersection Observer API.
|
||||
*
|
||||
* If true, the fields will be rendered immediately, rather than waiting for them to intersect with the viewport.
|
||||
*
|
||||
* If a number is provided, will immediately render fields _up to that index_.
|
||||
*/
|
||||
readonly forceRender?: boolean
|
||||
readonly margins?: 'small' | false
|
||||
readonly parentIndexPath: string
|
||||
readonly parentPath: string
|
||||
@@ -21,4 +13,4 @@ export type RenderFieldsProps = {
|
||||
}
|
||||
| SanitizedFieldPermissions
|
||||
readonly readOnly?: boolean
|
||||
}
|
||||
} & Pick<ClientComponentProps, 'forceRender'>
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { type Field, type FieldState, type ServerFunction } from 'payload'
|
||||
|
||||
import { getClientConfig } from '../../../utilities/getClientConfig.js'
|
||||
import { getClientSchemaMap } from '../../../utilities/getClientSchemaMap.js'
|
||||
import { getSchemaMap } from '../../../utilities/getSchemaMap.js'
|
||||
import { renderField } from '../renderField.js'
|
||||
|
||||
export type RenderFieldServerFnArgs = {
|
||||
/**
|
||||
* Override field config pulled from schemaPath lookup
|
||||
*/
|
||||
field?: Partial<Field>
|
||||
/**
|
||||
* Pass the value this field will receive when rendering it on the server.
|
||||
* For richText, this helps provide initial state for sub-fields that are immediately rendered (like blocks)
|
||||
* so that we can avoid multiple waterfall requests for each block that renders on the client.
|
||||
*/
|
||||
initialValue?: unknown
|
||||
/**
|
||||
* Path to the field to render
|
||||
* @default field name
|
||||
*/
|
||||
path?: string
|
||||
/**
|
||||
* chema path to the field to render. {global|collection}.entitySlug.fieldSchemaPath
|
||||
*
|
||||
* @example collection.posts.richText
|
||||
*/
|
||||
schemaPath: string
|
||||
}
|
||||
export type RenderFieldServerFnReturnType = {} & FieldState['customComponents']
|
||||
|
||||
/**
|
||||
* @experimental - may break in minor releases
|
||||
*/
|
||||
export const _internal_renderFieldHandler: ServerFunction<
|
||||
RenderFieldServerFnArgs,
|
||||
Promise<RenderFieldServerFnReturnType>
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
> = async ({ field: fieldArg, initialValue, path, req, schemaPath }) => {
|
||||
if (!req.user) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
const [entityType, entitySlug, ...fieldPath] = schemaPath.split('.')
|
||||
|
||||
const schemaMap = getSchemaMap({
|
||||
collectionSlug: entityType === 'collection' ? entitySlug : undefined,
|
||||
config: req.payload.config,
|
||||
globalSlug: entityType === 'global' ? entitySlug : undefined,
|
||||
i18n: req.i18n,
|
||||
})
|
||||
|
||||
// Provide client schema map as it would have been provided if the target editor field would have been rendered.
|
||||
// For lexical, only then will it contain all the lexical-internal entries
|
||||
const clientSchemaMap = getClientSchemaMap({
|
||||
collectionSlug: entityType === 'collection' ? entitySlug : undefined,
|
||||
config: getClientConfig({
|
||||
config: req.payload.config,
|
||||
i18n: req.i18n,
|
||||
importMap: req.payload.importMap,
|
||||
}),
|
||||
globalSlug: entityType === 'global' ? entitySlug : undefined,
|
||||
i18n: req.i18n,
|
||||
payload: req.payload,
|
||||
schemaMap,
|
||||
})
|
||||
|
||||
const targetField = schemaMap.get(`${entitySlug}.${fieldPath.join('.')}`) as Field | undefined
|
||||
|
||||
if (!targetField) {
|
||||
throw new Error(`Could not find target field at schemaPath: ${schemaPath}`)
|
||||
}
|
||||
|
||||
const field: Field = {
|
||||
...(targetField || {}),
|
||||
...(fieldArg || {}),
|
||||
} as Field
|
||||
|
||||
let data = {}
|
||||
if (typeof initialValue !== 'undefined') {
|
||||
if ('name' in field) {
|
||||
data[field.name] = initialValue
|
||||
} else {
|
||||
data = initialValue
|
||||
}
|
||||
}
|
||||
|
||||
const fieldState: FieldState = {}
|
||||
renderField({
|
||||
clientFieldSchemaMap: clientSchemaMap,
|
||||
collectionSlug: entityType === 'collection' && entitySlug ? entitySlug : '-',
|
||||
data,
|
||||
fieldConfig: field,
|
||||
fieldSchemaMap: schemaMap,
|
||||
fieldState, // TODO,
|
||||
formState: {}, // TODO,
|
||||
indexPath: '',
|
||||
lastRenderedPath: '',
|
||||
operation: 'create',
|
||||
parentPath: '',
|
||||
parentSchemaPath: '',
|
||||
path: path ?? ('name' in field ? field.name : ''),
|
||||
permissions: true,
|
||||
preferences: {
|
||||
fields: {},
|
||||
},
|
||||
previousFieldState: undefined,
|
||||
renderAllFields: true,
|
||||
req,
|
||||
schemaPath: `${entitySlug}.${fieldPath.join('.')}`,
|
||||
siblingData: data,
|
||||
})
|
||||
|
||||
return fieldState.customComponents ?? {}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { PayloadRequest } from 'payload'
|
||||
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
import React, { useCallback, useMemo, useRef } from 'react'
|
||||
|
||||
import type { UPDATE } from '../Form/types.js'
|
||||
import type { FieldType, Options } from './types.js'
|
||||
@@ -24,12 +24,7 @@ import {
|
||||
} from '../Form/context.js'
|
||||
import { useFieldPath } from '../RenderFields/context.js'
|
||||
|
||||
/**
|
||||
* Get and set the value of a form field.
|
||||
*
|
||||
* @see https://payloadcms.com/docs/admin/react-hooks#usefield
|
||||
*/
|
||||
export const useField = <TValue,>(options?: Options): FieldType<TValue> => {
|
||||
const useFieldInForm = <TValue,>(options?: Options): FieldType<TValue> => {
|
||||
const {
|
||||
disableFormData = false,
|
||||
hasRows,
|
||||
@@ -229,3 +224,47 @@ export const useField = <TValue,>(options?: Options): FieldType<TValue> => {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Context to allow providing useField value for fields directly, if managed outside the form
|
||||
*/
|
||||
export const FieldContext = React.createContext<FieldType<unknown> | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* Get and set the value of a form field.
|
||||
*
|
||||
* @see https://payloadcms.com/docs/admin/react-hooks#usefield
|
||||
*/
|
||||
export const useField = <TValue,>(options?: Options): FieldType<TValue> => {
|
||||
const pathFromContext = useFieldPath()
|
||||
|
||||
const ctx = React.use(FieldContext) as FieldType<TValue> | undefined
|
||||
|
||||
// Lock the mode on first render so hook order is stable forever. This ensures
|
||||
// that hooks are called in the same order each time a component renders => should
|
||||
// not break the rule of hooks.
|
||||
const modeRef = React.useRef<'context' | 'impl' | null>(null)
|
||||
if (modeRef.current === null) {
|
||||
// Use field context, if a field context exists **and** the path matches. If the path
|
||||
// does not match, this could be the field context of a parent field => there likely is
|
||||
// a nested <Form /> we should use instead => 'impl'
|
||||
const currentPath = options?.path || pathFromContext || options.potentiallyStalePath
|
||||
|
||||
modeRef.current = ctx && currentPath && ctx.path === currentPath ? 'context' : 'impl'
|
||||
}
|
||||
|
||||
if (modeRef.current === 'context') {
|
||||
if (!ctx) {
|
||||
// Provider was removed after mount. That violates hook guarantees.
|
||||
throw new Error('FieldContext was removed after mount. This breaks hook ordering.')
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
// We intentionally guard this hook call with a mode that is fixed on first render.
|
||||
// The order is consistent across renders. Silence the linter’s false positive.
|
||||
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return useFieldInForm<TValue>(options)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ import type {
|
||||
|
||||
import React, { createContext, useCallback } from 'react'
|
||||
|
||||
import type {
|
||||
RenderFieldServerFnArgs,
|
||||
RenderFieldServerFnReturnType,
|
||||
} from '../../forms/fieldSchemasToFormState/serverFunctions/renderFieldServerFn.js'
|
||||
import type { buildFormStateHandler } from '../../utilities/buildFormState.js'
|
||||
import type { buildTableStateHandler } from '../../utilities/buildTableState.js'
|
||||
import type { CopyDataFromLocaleArgs } from '../../utilities/copyDataFromLocale.js'
|
||||
@@ -100,7 +104,9 @@ type GetFolderResultsComponentAndDataClient = (
|
||||
} & Omit<GetFolderResultsComponentAndDataArgs, 'req'>,
|
||||
) => ReturnType<typeof getFolderResultsComponentAndDataHandler>
|
||||
|
||||
type ServerFunctionsContextType = {
|
||||
type RenderFieldClient = (args: RenderFieldServerFnArgs) => Promise<RenderFieldServerFnReturnType>
|
||||
export type ServerFunctionsContextType = {
|
||||
_internal_renderField: RenderFieldClient
|
||||
copyDataFromLocale: CopyDataFromLocaleClient
|
||||
getDocumentSlots: GetDocumentSlots
|
||||
getFolderResultsComponentAndData: GetFolderResultsComponentAndDataClient
|
||||
@@ -278,9 +284,26 @@ export const ServerFunctionsProvider: React.FC<{
|
||||
[serverFunction],
|
||||
)
|
||||
|
||||
const _internal_renderField = useCallback<RenderFieldClient>(
|
||||
async (args) => {
|
||||
try {
|
||||
const result = (await serverFunction({
|
||||
name: 'render-field',
|
||||
args,
|
||||
})) as RenderFieldServerFnReturnType
|
||||
|
||||
return result
|
||||
} catch (_err) {
|
||||
console.error(_err) // eslint-disable-line no-console
|
||||
}
|
||||
},
|
||||
[serverFunction],
|
||||
)
|
||||
|
||||
return (
|
||||
<ServerFunctionsContext
|
||||
value={{
|
||||
_internal_renderField,
|
||||
copyDataFromLocale,
|
||||
getDocumentSlots,
|
||||
getFolderResultsComponentAndData,
|
||||
|
||||
@@ -30,27 +30,7 @@ export const reduceFieldsToOptions = ({
|
||||
}: ReduceFieldOptionsArgs): ReducedField[] => {
|
||||
return fields.reduce((reduced, field) => {
|
||||
// Do not filter out `field.admin.disableListFilter` fields here, as these should still render as disabled if they appear in the URL query
|
||||
// Filter out `virtual: true` fields since they are regular virtuals and not backed by a DB field
|
||||
if (
|
||||
(fieldIsHiddenOrDisabled(field) && !fieldIsID(field)) ||
|
||||
('virtual' in field && field.virtual === true)
|
||||
) {
|
||||
return reduced
|
||||
}
|
||||
|
||||
// Handle virtual:string fields (virtual relationships, e.g. "post.title")
|
||||
if ('virtual' in field && typeof field.virtual === 'string') {
|
||||
const baseLabel = ('label' in field && field.label) || ('name' in field && field.name) || ''
|
||||
const localizedLabel = getTranslation(baseLabel, i18n)
|
||||
|
||||
reduced.push({
|
||||
label: localizedLabel,
|
||||
plainTextLabel: localizedLabel,
|
||||
value: field.virtual, // e.g. "post.title"
|
||||
...fieldTypes[field.type],
|
||||
field,
|
||||
operators: fieldTypes[field.type].operators,
|
||||
})
|
||||
if (fieldIsHiddenOrDisabled(field) && !fieldIsID(field)) {
|
||||
return reduced
|
||||
}
|
||||
|
||||
|
||||
1481
pnpm-lock.yaml
generated
1481
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,6 @@ import { getClientSideURL } from '@/utilities/getURL'
|
||||
export const getMediaUrl = (url: string | null | undefined, cacheTag?: string | null): string => {
|
||||
if (!url) return ''
|
||||
|
||||
if (cacheTag && cacheTag !== '') {
|
||||
cacheTag = encodeURIComponent(cacheTag)
|
||||
}
|
||||
|
||||
// Check if URL already has http/https protocol
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return cacheTag ? `${url}?${cacheTag}` : url
|
||||
|
||||
@@ -9,10 +9,6 @@ import { getClientSideURL } from '@/utilities/getURL'
|
||||
export const getMediaUrl = (url: string | null | undefined, cacheTag?: string | null): string => {
|
||||
if (!url) return ''
|
||||
|
||||
if (cacheTag && cacheTag !== '') {
|
||||
cacheTag = encodeURIComponent(cacheTag)
|
||||
}
|
||||
|
||||
// Check if URL already has http/https protocol
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return cacheTag ? `${url}?${cacheTag}` : url
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
|
||||
export const PostsCollection: CollectionConfig = {
|
||||
@@ -13,14 +15,11 @@ export const PostsCollection: CollectionConfig = {
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'array',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [...defaultFeatures],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -126,12 +126,21 @@ export interface UserAuthOperations {
|
||||
export interface Post {
|
||||
id: string;
|
||||
title?: string | null;
|
||||
array?:
|
||||
| {
|
||||
title?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
content?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -270,12 +279,7 @@ export interface PayloadMigration {
|
||||
*/
|
||||
export interface PostsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
array?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
id?: T;
|
||||
};
|
||||
content?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
import type { FieldAccess } from 'payload'
|
||||
|
||||
import { buildEditorState } from '@payloadcms/richtext-lexical'
|
||||
|
||||
import type { Config, User } from './payload-types.js'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { textToLexicalJSON } from '../lexical/collections/LexicalLocalized/textToLexicalJSON.js'
|
||||
import { Auth } from './collections/Auth/index.js'
|
||||
import { Disabled } from './collections/Disabled/index.js'
|
||||
import { Hooks } from './collections/hooks/index.js'
|
||||
@@ -718,33 +719,33 @@ export default buildConfigWithDefaults(
|
||||
await payload.create({
|
||||
collection: 'regression1',
|
||||
data: {
|
||||
richText4: textToLexicalJSON({ text: 'Text1' }),
|
||||
array: [{ art: textToLexicalJSON({ text: 'Text2' }) }],
|
||||
arrayWithAccessFalse: [{ richText6: textToLexicalJSON({ text: 'Text3' }) }],
|
||||
richText4: buildEditorState({ text: 'Text1' }),
|
||||
array: [{ art: buildEditorState({ text: 'Text2' }) }],
|
||||
arrayWithAccessFalse: [{ richText6: buildEditorState({ text: 'Text3' }) }],
|
||||
group1: {
|
||||
text: 'Text4',
|
||||
richText1: textToLexicalJSON({ text: 'Text5' }),
|
||||
richText1: buildEditorState({ text: 'Text5' }),
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
blockType: 'myBlock3',
|
||||
richText7: textToLexicalJSON({ text: 'Text6' }),
|
||||
richText7: buildEditorState({ text: 'Text6' }),
|
||||
blockName: 'My Block 1',
|
||||
},
|
||||
],
|
||||
blocks3: [
|
||||
{
|
||||
blockType: 'myBlock2',
|
||||
richText5: textToLexicalJSON({ text: 'Text7' }),
|
||||
richText5: buildEditorState({ text: 'Text7' }),
|
||||
blockName: 'My Block 2',
|
||||
},
|
||||
],
|
||||
tab1: {
|
||||
richText2: textToLexicalJSON({ text: 'Text8' }),
|
||||
richText2: buildEditorState({ text: 'Text8' }),
|
||||
blocks2: [
|
||||
{
|
||||
blockType: 'myBlock',
|
||||
richText3: textToLexicalJSON({ text: 'Text9' }),
|
||||
richText3: buildEditorState({ text: 'Text9' }),
|
||||
blockName: 'My Block 3',
|
||||
},
|
||||
],
|
||||
@@ -757,12 +758,12 @@ export default buildConfigWithDefaults(
|
||||
data: {
|
||||
array: [
|
||||
{
|
||||
richText2: textToLexicalJSON({ text: 'Text1' }),
|
||||
richText2: buildEditorState({ text: 'Text1' }),
|
||||
},
|
||||
],
|
||||
group: {
|
||||
text: 'Text2',
|
||||
richText1: textToLexicalJSON({ text: 'Text3' }),
|
||||
richText1: buildEditorState({ text: 'Text3' }),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { virtualsSlug } from '../slugs.js'
|
||||
|
||||
export const Virtuals: CollectionConfig = {
|
||||
slug: virtualsSlug,
|
||||
admin: {
|
||||
useAsTitle: 'virtualTitleFromPost',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'virtualTitleFromPost',
|
||||
type: 'text',
|
||||
virtual: 'post.title',
|
||||
},
|
||||
{
|
||||
name: 'virtualText',
|
||||
type: 'text',
|
||||
virtual: true,
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import { UploadCollection } from './collections/Upload.js'
|
||||
import { UploadTwoCollection } from './collections/UploadTwo.js'
|
||||
import { UseAsTitleGroupField } from './collections/UseAsTitleGroupField.js'
|
||||
import { Users } from './collections/Users.js'
|
||||
import { Virtuals } from './collections/Virtuals.js'
|
||||
import { with300Documents } from './collections/With300Documents.js'
|
||||
import { CustomGlobalViews1 } from './globals/CustomViews1.js'
|
||||
import { CustomGlobalViews2 } from './globals/CustomViews2.js'
|
||||
@@ -188,7 +187,6 @@ export default buildConfigWithDefaults({
|
||||
UseAsTitleGroupField,
|
||||
DisableBulkEdit,
|
||||
CustomListDrawer,
|
||||
Virtuals,
|
||||
],
|
||||
globals: [
|
||||
GlobalHidden,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { expect, test } from '@playwright/test'
|
||||
import { mapAsync } from 'payload'
|
||||
import * as qs from 'qs-esm'
|
||||
|
||||
import type { Config, Geo, Post, Virtual } from '../../payload-types.js'
|
||||
import type { Config, Geo, Post } from '../../payload-types.js'
|
||||
|
||||
import {
|
||||
ensureCompilationIsDone,
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
listDrawerSlug,
|
||||
placeholderCollectionSlug,
|
||||
postsCollectionSlug,
|
||||
virtualsSlug,
|
||||
with300DocumentsSlug,
|
||||
} from '../../slugs.js'
|
||||
|
||||
@@ -71,7 +70,6 @@ describe('List View', () => {
|
||||
let placeholderUrl: AdminUrlUtil
|
||||
let disableBulkEditUrl: AdminUrlUtil
|
||||
let user: any
|
||||
let virtualsUrl: AdminUrlUtil
|
||||
|
||||
let serverURL: string
|
||||
let adminRoutes: ReturnType<typeof getRoutes>
|
||||
@@ -96,7 +94,6 @@ describe('List View', () => {
|
||||
withListViewUrl = new AdminUrlUtil(serverURL, listDrawerSlug)
|
||||
placeholderUrl = new AdminUrlUtil(serverURL, placeholderCollectionSlug)
|
||||
disableBulkEditUrl = new AdminUrlUtil(serverURL, 'disable-bulk-edit')
|
||||
virtualsUrl = new AdminUrlUtil(serverURL, virtualsSlug)
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
initPageConsoleErrorCatch(page)
|
||||
@@ -419,44 +416,6 @@ describe('List View', () => {
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('should not allow search by virtual: true field in field dropdown', async () => {
|
||||
await page.goto(virtualsUrl.list)
|
||||
|
||||
await openListFilters(page, {})
|
||||
|
||||
const whereBuilder = page.locator('.where-builder')
|
||||
await whereBuilder.locator('.where-builder__add-first-filter').click()
|
||||
|
||||
const conditionField = whereBuilder.locator('.condition__field')
|
||||
await conditionField.click()
|
||||
|
||||
const menuList = conditionField.locator('.rs__menu-list')
|
||||
|
||||
// ensure the virtual field is not present
|
||||
await expect(menuList.locator('div', { hasText: exactText('Virtual Text') })).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('should allow to filter by virtual relationship field', async () => {
|
||||
const post1 = await createPost({ title: 'somePost' })
|
||||
const post2 = await createPost({ title: 'otherPost' })
|
||||
|
||||
await createVirtualDoc({ post: post1.id })
|
||||
await createVirtualDoc({ post: post2.id })
|
||||
|
||||
await page.goto(virtualsUrl.list)
|
||||
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(2)
|
||||
|
||||
await addListFilter({
|
||||
page,
|
||||
fieldLabel: 'Virtual Title From Post',
|
||||
operatorLabel: 'equals',
|
||||
value: 'somePost',
|
||||
})
|
||||
|
||||
await expect(page.locator(tableRowLocator)).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('should allow to filter in array field', async () => {
|
||||
await createArray()
|
||||
|
||||
@@ -1827,13 +1786,3 @@ async function createArray() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function createVirtualDoc(overrides?: Partial<Virtual>): Promise<Virtual> {
|
||||
return payload.create({
|
||||
collection: virtualsSlug,
|
||||
data: {
|
||||
post: overrides?.post,
|
||||
...overrides,
|
||||
},
|
||||
}) as unknown as Promise<Virtual>
|
||||
}
|
||||
|
||||
@@ -94,7 +94,6 @@ export interface Config {
|
||||
'use-as-title-group-field': UseAsTitleGroupField;
|
||||
'disable-bulk-edit': DisableBulkEdit;
|
||||
'custom-list-drawer': CustomListDrawer;
|
||||
virtuals: Virtual;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
@@ -128,7 +127,6 @@ export interface Config {
|
||||
'use-as-title-group-field': UseAsTitleGroupFieldSelect<false> | UseAsTitleGroupFieldSelect<true>;
|
||||
'disable-bulk-edit': DisableBulkEditSelect<false> | DisableBulkEditSelect<true>;
|
||||
'custom-list-drawer': CustomListDrawerSelect<false> | CustomListDrawerSelect<true>;
|
||||
virtuals: VirtualsSelect<false> | VirtualsSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
@@ -584,18 +582,6 @@ export interface CustomListDrawer {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "virtuals".
|
||||
*/
|
||||
export interface Virtual {
|
||||
id: string;
|
||||
virtualTitleFromPost?: string | null;
|
||||
virtualText?: string | null;
|
||||
post?: (string | null) | Post;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
@@ -710,10 +696,6 @@ export interface PayloadLockedDocument {
|
||||
| ({
|
||||
relationTo: 'custom-list-drawer';
|
||||
value: string | CustomListDrawer;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'virtuals';
|
||||
value: string | Virtual;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
@@ -1129,17 +1111,6 @@ export interface CustomListDrawerSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "virtuals_select".
|
||||
*/
|
||||
export interface VirtualsSelect<T extends boolean = true> {
|
||||
virtualTitleFromPost?: T;
|
||||
virtualText?: T;
|
||||
post?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
|
||||
@@ -23,7 +23,6 @@ export const uploadTwoCollectionSlug = 'uploads-two'
|
||||
export const customFieldsSlug = 'custom-fields'
|
||||
|
||||
export const listDrawerSlug = 'with-list-drawer'
|
||||
export const virtualsSlug = 'virtuals'
|
||||
export const collectionSlugs = [
|
||||
usersCollectionSlug,
|
||||
customViews1CollectionSlug,
|
||||
@@ -40,7 +39,6 @@ export const collectionSlugs = [
|
||||
customFieldsSlug,
|
||||
disableDuplicateSlug,
|
||||
listDrawerSlug,
|
||||
virtualsSlug,
|
||||
]
|
||||
|
||||
export const customGlobalViews1GlobalSlug = 'custom-global-views-one'
|
||||
|
||||
@@ -75,10 +75,6 @@ export default buildConfigWithDefaults({
|
||||
label: 'Named Save To JWT',
|
||||
saveToJWT: saveToJWTKey,
|
||||
},
|
||||
{
|
||||
name: 'richText',
|
||||
type: 'richText',
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
|
||||
@@ -124,33 +124,6 @@ describe('Auth', () => {
|
||||
.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT })
|
||||
.not.toContain('create-first-user')
|
||||
})
|
||||
|
||||
test('richText field should should not be readOnly in create first user view', async () => {
|
||||
const {
|
||||
admin: {
|
||||
routes: { createFirstUser: createFirstUserRoute },
|
||||
},
|
||||
routes: { admin: adminRoute },
|
||||
} = getRoutes({})
|
||||
|
||||
// wait for create first user route
|
||||
await page.goto(serverURL + `${adminRoute}${createFirstUserRoute}`)
|
||||
|
||||
await expect(page.locator('.create-first-user')).toBeVisible()
|
||||
|
||||
await waitForVisibleAuthFields()
|
||||
|
||||
const richTextRoot = page
|
||||
.locator('.rich-text-lexical .ContentEditable__root[data-lexical-editor="true"]')
|
||||
.first()
|
||||
|
||||
// ensure editor is present
|
||||
await expect(richTextRoot).toBeVisible()
|
||||
|
||||
// core read-only checks
|
||||
await expect(richTextRoot).toHaveAttribute('contenteditable', 'true')
|
||||
await expect(richTextRoot).not.toHaveAttribute('aria-readonly', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
describe('non create first user', () => {
|
||||
|
||||
@@ -243,21 +243,6 @@ export interface User {
|
||||
adminOnlyField?: string | null;
|
||||
roles: ('admin' | 'editor' | 'moderator' | 'user' | 'viewer')[];
|
||||
namedSaveToJWT?: string | null;
|
||||
richText?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
group?: {
|
||||
liftedSaveToJWT?: string | null;
|
||||
};
|
||||
@@ -518,7 +503,6 @@ export interface UsersSelect<T extends boolean = true> {
|
||||
adminOnlyField?: T;
|
||||
roles?: T;
|
||||
namedSaveToJWT?: T;
|
||||
richText?: T;
|
||||
group?:
|
||||
| T
|
||||
| {
|
||||
|
||||
@@ -2,14 +2,8 @@ import type { BrowserContext, Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { copyPasteField } from 'helpers/e2e/copyPasteField.js'
|
||||
import { duplicateArrayRow } from 'helpers/e2e/fields/array/index.js'
|
||||
import {
|
||||
addBlock,
|
||||
addBlockBelow,
|
||||
duplicateBlock,
|
||||
openBlocksDrawer,
|
||||
reorderBlocks,
|
||||
} from 'helpers/e2e/fields/blocks/index.js'
|
||||
import { addArrayRowBelow, duplicateArrayRow } from 'helpers/e2e/fields/array/index.js'
|
||||
import { addBlock, openBlocksDrawer, reorderBlocks } from 'helpers/e2e/fields/blocks/index.js'
|
||||
import { scrollEntirePage } from 'helpers/e2e/scrollEntirePage.js'
|
||||
import { toggleBlockOrArrayRow } from 'helpers/e2e/toggleCollapsible.js'
|
||||
import path from 'path'
|
||||
@@ -133,13 +127,22 @@ describe('Block fields', () => {
|
||||
test('should open blocks drawer from block row and add below', async () => {
|
||||
await page.goto(url.create)
|
||||
|
||||
await addBlockBelow(page, { fieldName: 'blocks', blockToSelect: 'Content' })
|
||||
await addArrayRowBelow(page, { fieldName: 'blocks' })
|
||||
|
||||
const blocksDrawer = page.locator('[id^=drawer_1_blocks-drawer-]')
|
||||
await expect(blocksDrawer).toBeVisible()
|
||||
|
||||
// select the first block in the drawer
|
||||
const firstBlockSelector = blocksDrawer
|
||||
.locator('.blocks-drawer__blocks .blocks-drawer__block')
|
||||
.first()
|
||||
|
||||
await expect(firstBlockSelector).toContainText('Content')
|
||||
await firstBlockSelector.click()
|
||||
|
||||
// ensure the block was inserted beneath the first in the rows
|
||||
const addedRow = page.locator('#field-blocks #blocks-row-1')
|
||||
|
||||
await expect(addedRow).toBeVisible()
|
||||
|
||||
await expect(addedRow.locator('.blocks-field__block-header')).toHaveText(
|
||||
'Custom Block Label: Content 02',
|
||||
) // went from `Number` to `Content`
|
||||
@@ -148,17 +151,19 @@ describe('Block fields', () => {
|
||||
test('should duplicate block', async () => {
|
||||
await page.goto(url.create)
|
||||
|
||||
const { rowCount } = await duplicateBlock(page, { fieldName: 'blocks' })
|
||||
await duplicateArrayRow(page, { fieldName: 'blocks' })
|
||||
|
||||
expect(rowCount).toEqual(5)
|
||||
const blocks = page.locator('#field-blocks > .blocks-field__rows > div')
|
||||
expect(await blocks.count()).toEqual(5)
|
||||
})
|
||||
|
||||
test('should save when duplicating subblocks', async () => {
|
||||
await page.goto(url.create)
|
||||
|
||||
const { rowCount } = await duplicateBlock(page, { fieldName: 'blocks', rowIndex: 2 })
|
||||
await duplicateArrayRow(page, { fieldName: 'blocks', rowIndex: 2 })
|
||||
|
||||
expect(rowCount).toEqual(5)
|
||||
const blocks = page.locator('#field-blocks > .blocks-field__rows > div')
|
||||
expect(await blocks.count()).toEqual(5)
|
||||
|
||||
await page.click('#action-save')
|
||||
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
||||
|
||||
@@ -149,7 +149,7 @@ export interface Config {
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
defaultIDType: number;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
@@ -215,7 +215,7 @@ export interface LocalizedTextReference2 {
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
id: number;
|
||||
canViewConditionalField?: boolean | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -240,7 +240,7 @@ export interface User {
|
||||
* via the `definition` "select-versions-fields".
|
||||
*/
|
||||
export interface SelectVersionsField {
|
||||
id: string;
|
||||
id: number;
|
||||
hasMany?: ('a' | 'b' | 'c' | 'd')[] | null;
|
||||
array?:
|
||||
| {
|
||||
@@ -265,7 +265,7 @@ export interface SelectVersionsField {
|
||||
* via the `definition` "array-fields".
|
||||
*/
|
||||
export interface ArrayField {
|
||||
id: string;
|
||||
id: number;
|
||||
title?: string | null;
|
||||
items: {
|
||||
text: string;
|
||||
@@ -369,7 +369,7 @@ export interface ArrayField {
|
||||
* via the `definition` "block-fields".
|
||||
*/
|
||||
export interface BlockField {
|
||||
id: string;
|
||||
id: number;
|
||||
blocks: (ContentBlock | NoBlockname | NumberBlock | SubBlocksBlock | TabsBlock)[];
|
||||
duplicate: (ContentBlock | NoBlockname | NumberBlock | SubBlocksBlock | TabsBlock)[];
|
||||
collapsedByDefaultBlocks: (
|
||||
@@ -500,7 +500,7 @@ export interface BlockField {
|
||||
| null;
|
||||
relationshipBlocks?:
|
||||
| {
|
||||
relationship?: (string | null) | TextField;
|
||||
relationship?: (number | null) | TextField;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'relationships';
|
||||
@@ -697,7 +697,7 @@ export interface LocalizedTabsBlock {
|
||||
* via the `definition` "text-fields".
|
||||
*/
|
||||
export interface TextField {
|
||||
id: string;
|
||||
id: number;
|
||||
text: string;
|
||||
hiddenTextField?: string | null;
|
||||
/**
|
||||
@@ -749,7 +749,7 @@ export interface TextField {
|
||||
* via the `definition` "checkbox-fields".
|
||||
*/
|
||||
export interface CheckboxField {
|
||||
id: string;
|
||||
id: number;
|
||||
checkbox: boolean;
|
||||
checkboxNotRequired?: boolean | null;
|
||||
updatedAt: string;
|
||||
@@ -760,7 +760,7 @@ export interface CheckboxField {
|
||||
* via the `definition` "code-fields".
|
||||
*/
|
||||
export interface CodeField {
|
||||
id: string;
|
||||
id: number;
|
||||
javascript?: string | null;
|
||||
typescript?: string | null;
|
||||
json?: string | null;
|
||||
@@ -775,7 +775,7 @@ export interface CodeField {
|
||||
* via the `definition` "collapsible-fields".
|
||||
*/
|
||||
export interface CollapsibleField {
|
||||
id: string;
|
||||
id: number;
|
||||
text: string;
|
||||
group: {
|
||||
textWithinGroup?: string | null;
|
||||
@@ -808,7 +808,7 @@ export interface CollapsibleField {
|
||||
* via the `definition` "conditional-logic".
|
||||
*/
|
||||
export interface ConditionalLogic {
|
||||
id: string;
|
||||
id: number;
|
||||
text: string;
|
||||
toggleField?: boolean | null;
|
||||
fieldWithDocIDCondition?: string | null;
|
||||
@@ -922,7 +922,7 @@ export interface CustomRowId {
|
||||
* via the `definition` "date-fields".
|
||||
*/
|
||||
export interface DateField {
|
||||
id: string;
|
||||
id: number;
|
||||
default: string;
|
||||
timeOnly?: string | null;
|
||||
timeOnlyWithMiliseconds?: string | null;
|
||||
@@ -967,7 +967,7 @@ export interface DateField {
|
||||
* via the `definition` "email-fields".
|
||||
*/
|
||||
export interface EmailField {
|
||||
id: string;
|
||||
id: number;
|
||||
email: string;
|
||||
localizedEmail?: string | null;
|
||||
emailWithAutocomplete?: string | null;
|
||||
@@ -992,7 +992,7 @@ export interface EmailField {
|
||||
* via the `definition` "radio-fields".
|
||||
*/
|
||||
export interface RadioField {
|
||||
id: string;
|
||||
id: number;
|
||||
radio?: ('one' | 'two' | 'three') | null;
|
||||
radioWithJsxLabelOption?: ('one' | 'two' | 'three') | null;
|
||||
updatedAt: string;
|
||||
@@ -1003,7 +1003,7 @@ export interface RadioField {
|
||||
* via the `definition` "group-fields".
|
||||
*/
|
||||
export interface GroupField {
|
||||
id: string;
|
||||
id: number;
|
||||
/**
|
||||
* This is a group.
|
||||
*/
|
||||
@@ -1085,22 +1085,22 @@ export interface GroupField {
|
||||
select?: ('one' | 'two')[] | null;
|
||||
};
|
||||
localizedGroupRel?: {
|
||||
email?: (string | null) | EmailField;
|
||||
email?: (number | null) | EmailField;
|
||||
};
|
||||
localizedGroupManyRel?: {
|
||||
email?: (string | EmailField)[] | null;
|
||||
email?: (number | EmailField)[] | null;
|
||||
};
|
||||
localizedGroupPolyRel?: {
|
||||
email?: {
|
||||
relationTo: 'email-fields';
|
||||
value: string | EmailField;
|
||||
value: number | EmailField;
|
||||
} | null;
|
||||
};
|
||||
localizedGroupPolyHasManyRel?: {
|
||||
email?:
|
||||
| {
|
||||
relationTo: 'email-fields';
|
||||
value: string | EmailField;
|
||||
value: number | EmailField;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
@@ -1154,30 +1154,30 @@ export interface RowField {
|
||||
* via the `definition` "indexed-fields".
|
||||
*/
|
||||
export interface IndexedField {
|
||||
id: string;
|
||||
id: number;
|
||||
text: string;
|
||||
uniqueText?: string | null;
|
||||
uniqueRelationship?: (string | null) | TextField;
|
||||
uniqueHasManyRelationship?: (string | TextField)[] | null;
|
||||
uniqueHasManyRelationship_2?: (string | TextField)[] | null;
|
||||
uniqueRelationship?: (number | null) | TextField;
|
||||
uniqueHasManyRelationship?: (number | TextField)[] | null;
|
||||
uniqueHasManyRelationship_2?: (number | TextField)[] | null;
|
||||
uniquePolymorphicRelationship?: {
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
value: number | TextField;
|
||||
} | null;
|
||||
uniquePolymorphicRelationship_2?: {
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
value: number | TextField;
|
||||
} | null;
|
||||
uniqueHasManyPolymorphicRelationship?:
|
||||
| {
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
value: number | TextField;
|
||||
}[]
|
||||
| null;
|
||||
uniqueHasManyPolymorphicRelationship_2?:
|
||||
| {
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
value: number | TextField;
|
||||
}[]
|
||||
| null;
|
||||
uniqueRequiredText: string;
|
||||
@@ -1213,7 +1213,7 @@ export interface IndexedField {
|
||||
* via the `definition` "json-fields".
|
||||
*/
|
||||
export interface JsonField {
|
||||
id: string;
|
||||
id: number;
|
||||
json?: {
|
||||
array?: {
|
||||
object?: {
|
||||
@@ -1254,7 +1254,7 @@ export interface JsonField {
|
||||
* via the `definition` "number-fields".
|
||||
*/
|
||||
export interface NumberField {
|
||||
id: string;
|
||||
id: number;
|
||||
number?: number | null;
|
||||
min?: number | null;
|
||||
max?: number | null;
|
||||
@@ -1289,7 +1289,7 @@ export interface NumberField {
|
||||
* via the `definition` "point-fields".
|
||||
*/
|
||||
export interface PointField {
|
||||
id: string;
|
||||
id: number;
|
||||
/**
|
||||
* @minItems 2
|
||||
* @maxItems 2
|
||||
@@ -1320,83 +1320,83 @@ export interface PointField {
|
||||
* via the `definition` "relationship-fields".
|
||||
*/
|
||||
export interface RelationshipField {
|
||||
id: string;
|
||||
id: number;
|
||||
text?: string | null;
|
||||
relationship:
|
||||
| {
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
value: number | TextField;
|
||||
}
|
||||
| {
|
||||
relationTo: 'array-fields';
|
||||
value: string | ArrayField;
|
||||
value: number | ArrayField;
|
||||
};
|
||||
relationHasManyPolymorphic?:
|
||||
| (
|
||||
| {
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
value: number | TextField;
|
||||
}
|
||||
| {
|
||||
relationTo: 'array-fields';
|
||||
value: string | ArrayField;
|
||||
value: number | ArrayField;
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
relationToSelf?: (string | null) | RelationshipField;
|
||||
relationToSelfSelectOnly?: (string | null) | RelationshipField;
|
||||
relationWithAllowCreateToFalse?: (string | null) | User;
|
||||
relationWithAllowEditToFalse?: (string | null) | User;
|
||||
relationWithDynamicDefault?: (string | null) | User;
|
||||
relationToSelf?: (number | null) | RelationshipField;
|
||||
relationToSelfSelectOnly?: (number | null) | RelationshipField;
|
||||
relationWithAllowCreateToFalse?: (number | null) | User;
|
||||
relationWithAllowEditToFalse?: (number | null) | User;
|
||||
relationWithDynamicDefault?: (number | null) | User;
|
||||
relationHasManyWithDynamicDefault?: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
} | null;
|
||||
relationshipWithMin?: (string | TextField)[] | null;
|
||||
relationshipWithMax?: (string | TextField)[] | null;
|
||||
relationshipHasMany?: (string | TextField)[] | null;
|
||||
relationshipWithMin?: (number | TextField)[] | null;
|
||||
relationshipWithMax?: (number | TextField)[] | null;
|
||||
relationshipHasMany?: (number | TextField)[] | null;
|
||||
array?:
|
||||
| {
|
||||
relationship?: (string | null) | TextField;
|
||||
relationship?: (number | null) | TextField;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
relationshipWithMinRows?:
|
||||
| {
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
value: number | TextField;
|
||||
}[]
|
||||
| null;
|
||||
relationToRow?: (string | null) | RowField;
|
||||
relationToRowMany?: (string | RowField)[] | null;
|
||||
relationshipDrawer?: (string | null) | TextField;
|
||||
relationshipDrawerReadOnly?: (string | null) | TextField;
|
||||
relationshipDrawer?: (number | null) | TextField;
|
||||
relationshipDrawerReadOnly?: (number | null) | TextField;
|
||||
polymorphicRelationshipDrawer?:
|
||||
| ({
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
value: number | TextField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'array-fields';
|
||||
value: string | ArrayField;
|
||||
value: number | ArrayField;
|
||||
} | null);
|
||||
relationshipDrawerHasMany?: (string | TextField)[] | null;
|
||||
relationshipDrawerHasMany?: (number | TextField)[] | null;
|
||||
relationshipDrawerHasManyPolymorphic?:
|
||||
| (
|
||||
| {
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
value: number | TextField;
|
||||
}
|
||||
| {
|
||||
relationTo: 'array-fields';
|
||||
value: string | ArrayField;
|
||||
value: number | ArrayField;
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
relationshipDrawerWithAllowCreateFalse?: (string | null) | TextField;
|
||||
relationshipDrawerWithAllowCreateFalse?: (number | null) | TextField;
|
||||
relationshipDrawerWithFilterOptions?: {
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
value: number | TextField;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -1406,7 +1406,7 @@ export interface RelationshipField {
|
||||
* via the `definition` "select-fields".
|
||||
*/
|
||||
export interface SelectField {
|
||||
id: string;
|
||||
id: number;
|
||||
select?: ('one' | 'two' | 'three') | null;
|
||||
selectReadOnly?: ('one' | 'two' | 'three') | null;
|
||||
selectHasMany?: ('one' | 'two' | 'three' | 'four' | 'five' | 'six')[] | null;
|
||||
@@ -1436,7 +1436,7 @@ export interface SelectField {
|
||||
* via the `definition` "tabs-fields-2".
|
||||
*/
|
||||
export interface TabsFields2 {
|
||||
id: string;
|
||||
id: number;
|
||||
tabsInArray?:
|
||||
| {
|
||||
text?: string | null;
|
||||
@@ -1454,7 +1454,7 @@ export interface TabsFields2 {
|
||||
* via the `definition` "tabs-fields".
|
||||
*/
|
||||
export interface TabsField {
|
||||
id: string;
|
||||
id: number;
|
||||
/**
|
||||
* This should not collapse despite there being many tabs pushing the main fields open.
|
||||
*/
|
||||
@@ -1556,9 +1556,9 @@ export interface TabWithName {
|
||||
* via the `definition` "uploads".
|
||||
*/
|
||||
export interface Upload {
|
||||
id: string;
|
||||
id: number;
|
||||
text?: string | null;
|
||||
media?: (string | null) | Upload;
|
||||
media?: (number | null) | Upload;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
@@ -1576,9 +1576,9 @@ export interface Upload {
|
||||
* via the `definition` "uploads2".
|
||||
*/
|
||||
export interface Uploads2 {
|
||||
id: string;
|
||||
id: number;
|
||||
text?: string | null;
|
||||
media?: (string | null) | Uploads2;
|
||||
media?: (number | null) | Uploads2;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
@@ -1596,8 +1596,8 @@ export interface Uploads2 {
|
||||
* via the `definition` "uploads3".
|
||||
*/
|
||||
export interface Uploads3 {
|
||||
id: string;
|
||||
media?: (string | null) | Uploads3;
|
||||
id: number;
|
||||
media?: (number | null) | Uploads3;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
@@ -1615,9 +1615,9 @@ export interface Uploads3 {
|
||||
* via the `definition` "uploads-multi".
|
||||
*/
|
||||
export interface UploadsMulti {
|
||||
id: string;
|
||||
id: number;
|
||||
text?: string | null;
|
||||
media?: (string | Upload)[] | null;
|
||||
media?: (number | Upload)[] | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -1626,16 +1626,16 @@ export interface UploadsMulti {
|
||||
* via the `definition` "uploads-poly".
|
||||
*/
|
||||
export interface UploadsPoly {
|
||||
id: string;
|
||||
id: number;
|
||||
text?: string | null;
|
||||
media?:
|
||||
| ({
|
||||
relationTo: 'uploads';
|
||||
value: string | Upload;
|
||||
value: number | Upload;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'uploads2';
|
||||
value: string | Uploads2;
|
||||
value: number | Uploads2;
|
||||
} | null);
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -1645,17 +1645,17 @@ export interface UploadsPoly {
|
||||
* via the `definition` "uploads-multi-poly".
|
||||
*/
|
||||
export interface UploadsMultiPoly {
|
||||
id: string;
|
||||
id: number;
|
||||
text?: string | null;
|
||||
media?:
|
||||
| (
|
||||
| {
|
||||
relationTo: 'uploads';
|
||||
value: string | Upload;
|
||||
value: number | Upload;
|
||||
}
|
||||
| {
|
||||
relationTo: 'uploads2';
|
||||
value: string | Uploads2;
|
||||
value: number | Uploads2;
|
||||
}
|
||||
)[]
|
||||
| null;
|
||||
@@ -1667,11 +1667,11 @@ export interface UploadsMultiPoly {
|
||||
* via the `definition` "uploads-restricted".
|
||||
*/
|
||||
export interface UploadsRestricted {
|
||||
id: string;
|
||||
id: number;
|
||||
text?: string | null;
|
||||
uploadWithoutRestriction?: (string | null) | Upload;
|
||||
uploadWithAllowCreateFalse?: (string | null) | Upload;
|
||||
uploadMultipleWithAllowCreateFalse?: (string | Upload)[] | null;
|
||||
uploadWithoutRestriction?: (number | null) | Upload;
|
||||
uploadWithAllowCreateFalse?: (number | null) | Upload;
|
||||
uploadMultipleWithAllowCreateFalse?: (number | Upload)[] | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -1680,7 +1680,7 @@ export interface UploadsRestricted {
|
||||
* via the `definition` "ui-fields".
|
||||
*/
|
||||
export interface UiField {
|
||||
id: string;
|
||||
id: number;
|
||||
text: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -1690,39 +1690,39 @@ export interface UiField {
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
id: number;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'select-versions-fields';
|
||||
value: string | SelectVersionsField;
|
||||
value: number | SelectVersionsField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'array-fields';
|
||||
value: string | ArrayField;
|
||||
value: number | ArrayField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'block-fields';
|
||||
value: string | BlockField;
|
||||
value: number | BlockField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'checkbox-fields';
|
||||
value: string | CheckboxField;
|
||||
value: number | CheckboxField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'code-fields';
|
||||
value: string | CodeField;
|
||||
value: number | CodeField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'collapsible-fields';
|
||||
value: string | CollapsibleField;
|
||||
value: number | CollapsibleField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'conditional-logic';
|
||||
value: string | ConditionalLogic;
|
||||
value: number | ConditionalLogic;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'custom-id';
|
||||
@@ -1730,27 +1730,27 @@ export interface PayloadLockedDocument {
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'custom-tab-id';
|
||||
value: string | CustomTabId;
|
||||
value: number | CustomTabId;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'custom-row-id';
|
||||
value: string | CustomRowId;
|
||||
value: number | CustomRowId;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'date-fields';
|
||||
value: string | DateField;
|
||||
value: number | DateField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'email-fields';
|
||||
value: string | EmailField;
|
||||
value: number | EmailField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'radio-fields';
|
||||
value: string | RadioField;
|
||||
value: number | RadioField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'group-fields';
|
||||
value: string | GroupField;
|
||||
value: number | GroupField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'row-fields';
|
||||
@@ -1758,76 +1758,76 @@ export interface PayloadLockedDocument {
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'indexed-fields';
|
||||
value: string | IndexedField;
|
||||
value: number | IndexedField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'json-fields';
|
||||
value: string | JsonField;
|
||||
value: number | JsonField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'number-fields';
|
||||
value: string | NumberField;
|
||||
value: number | NumberField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'point-fields';
|
||||
value: string | PointField;
|
||||
value: number | PointField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'relationship-fields';
|
||||
value: string | RelationshipField;
|
||||
value: number | RelationshipField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'select-fields';
|
||||
value: string | SelectField;
|
||||
value: number | SelectField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'tabs-fields-2';
|
||||
value: string | TabsFields2;
|
||||
value: number | TabsFields2;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'tabs-fields';
|
||||
value: string | TabsField;
|
||||
value: number | TabsField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
value: number | TextField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'uploads';
|
||||
value: string | Upload;
|
||||
value: number | Upload;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'uploads2';
|
||||
value: string | Uploads2;
|
||||
value: number | Uploads2;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'uploads3';
|
||||
value: string | Uploads3;
|
||||
value: number | Uploads3;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'uploads-multi';
|
||||
value: string | UploadsMulti;
|
||||
value: number | UploadsMulti;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'uploads-poly';
|
||||
value: string | UploadsPoly;
|
||||
value: number | UploadsPoly;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'uploads-multi-poly';
|
||||
value: string | UploadsMultiPoly;
|
||||
value: number | UploadsMultiPoly;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'uploads-restricted';
|
||||
value: string | UploadsRestricted;
|
||||
value: number | UploadsRestricted;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'ui-fields';
|
||||
value: string | UiField;
|
||||
value: number | UiField;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -1837,10 +1837,10 @@ export interface PayloadLockedDocument {
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
id: number;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
@@ -1860,7 +1860,7 @@ export interface PayloadPreference {
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
id: number;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
export const ArrayRowLabel = (props) => {
|
||||
return (
|
||||
<p data-id={props.value[props?.rowNumber - 1]?.id} id="custom-array-row-label">
|
||||
This is a custom component
|
||||
</p>
|
||||
)
|
||||
export const ArrayRowLabel = () => {
|
||||
return <p id="custom-array-row-label">This is a custom component</p>
|
||||
}
|
||||
|
||||
@@ -6,12 +6,7 @@ import { expect, test } from '@playwright/test'
|
||||
import { assertElementStaysVisible } from 'helpers/e2e/assertElementStaysVisible.js'
|
||||
import { assertNetworkRequests } from 'helpers/e2e/assertNetworkRequests.js'
|
||||
import { assertRequestBody } from 'helpers/e2e/assertRequestBody.js'
|
||||
import {
|
||||
addArrayRow,
|
||||
addArrayRowAsync,
|
||||
duplicateArrayRow,
|
||||
removeArrayRow,
|
||||
} from 'helpers/e2e/fields/array/index.js'
|
||||
import { addArrayRowAsync, removeArrayRow } from 'helpers/e2e/fields/array/index.js'
|
||||
import { addBlock } from 'helpers/e2e/fields/blocks/index.js'
|
||||
import { waitForAutoSaveToRunAndComplete } from 'helpers/e2e/waitForAutoSaveToRunAndComplete.js'
|
||||
import * as path from 'path'
|
||||
@@ -459,34 +454,6 @@ test.describe('Form State', () => {
|
||||
await expect(computedTitleField).toHaveValue('Test Title 2')
|
||||
})
|
||||
|
||||
test('array and block rows and maintain consistent row IDs across duplication', async () => {
|
||||
await page.goto(postsUrl.create)
|
||||
await addArrayRow(page, { fieldName: 'array' })
|
||||
|
||||
const row0 = page.locator('#field-array #array-row-0')
|
||||
|
||||
await expect(row0.locator('#custom-array-row-label')).toHaveAttribute('data-id')
|
||||
|
||||
await expect(row0.locator('#field-array__0__id')).toHaveValue(
|
||||
(await row0.locator('#custom-array-row-label').getAttribute('data-id'))!,
|
||||
)
|
||||
|
||||
await duplicateArrayRow(page, { fieldName: 'array' })
|
||||
|
||||
const row1 = page.locator('#field-array #array-row-1')
|
||||
|
||||
await expect(row1.locator('#custom-array-row-label')).toHaveAttribute('data-id')
|
||||
|
||||
await expect(row1.locator('#custom-array-row-label')).not.toHaveAttribute(
|
||||
'data-id',
|
||||
(await row0.locator('#custom-array-row-label').getAttribute('data-id'))!,
|
||||
)
|
||||
|
||||
await expect(row1.locator('#field-array__1__id')).toHaveValue(
|
||||
(await row1.locator('#custom-array-row-label').getAttribute('data-id'))!,
|
||||
)
|
||||
})
|
||||
|
||||
describe('Throttled tests', () => {
|
||||
let cdpSession: CDPSession
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Locator, Page } from 'playwright'
|
||||
|
||||
import { wait } from 'payload/shared'
|
||||
import { expect } from 'playwright/test'
|
||||
|
||||
import { openArrayRowActions } from './openArrayRowActions.js'
|
||||
|
||||
@@ -19,12 +18,10 @@ export const addArrayRow = async (
|
||||
page: Page,
|
||||
{ fieldName }: Omit<Parameters<typeof openArrayRowActions>[1], 'rowIndex'>,
|
||||
) => {
|
||||
const rowLocator = page.locator(`#field-${fieldName} .array-field__row`)
|
||||
const numberOfPrevRows = await rowLocator.count()
|
||||
|
||||
await addArrayRowAsync(page, fieldName)
|
||||
|
||||
expect(await rowLocator.count()).toBe(numberOfPrevRows + 1)
|
||||
// TODO: test the array row has appeared
|
||||
await wait(300)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,19 +31,16 @@ export const addArrayRowBelow = async (
|
||||
page: Page,
|
||||
{ fieldName, rowIndex = 0 }: Parameters<typeof openArrayRowActions>[1],
|
||||
): Promise<{ popupContentLocator: Locator; rowActionsButtonLocator: Locator }> => {
|
||||
const rowLocator = page.locator(`#field-${fieldName} .array-field__row`)
|
||||
const numberOfPrevRows = await rowLocator.count()
|
||||
|
||||
const { popupContentLocator, rowActionsButtonLocator } = await openArrayRowActions(page, {
|
||||
fieldName,
|
||||
rowIndex,
|
||||
})
|
||||
|
||||
await popupContentLocator.locator('.array-actions__action.array-actions__add').click()
|
||||
const addBelowButton = popupContentLocator.locator('.array-actions__action.array-actions__add')
|
||||
|
||||
await expect(rowLocator).toHaveCount(numberOfPrevRows + 1)
|
||||
await addBelowButton.click()
|
||||
|
||||
// TODO: test the array row has appeared in the _correct position_ (immediately below the original row)
|
||||
// TODO: test the array row has appeared
|
||||
await wait(300)
|
||||
|
||||
return { popupContentLocator, rowActionsButtonLocator }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { Locator, Page } from 'playwright'
|
||||
|
||||
import { expect } from 'playwright/test'
|
||||
|
||||
import { openArrayRowActions } from './openArrayRowActions.js'
|
||||
|
||||
/**
|
||||
@@ -14,9 +12,6 @@ export const duplicateArrayRow = async (
|
||||
popupContentLocator: Locator
|
||||
rowActionsButtonLocator: Locator
|
||||
}> => {
|
||||
const rowLocator = page.locator(`#field-${fieldName} .array-field__row`)
|
||||
const numberOfPrevRows = await rowLocator.count()
|
||||
|
||||
const { popupContentLocator, rowActionsButtonLocator } = await openArrayRowActions(page, {
|
||||
fieldName,
|
||||
rowIndex,
|
||||
@@ -24,9 +19,7 @@ export const duplicateArrayRow = async (
|
||||
|
||||
await popupContentLocator.locator('.array-actions__action.array-actions__duplicate').click()
|
||||
|
||||
expect(await rowLocator.count()).toBe(numberOfPrevRows + 1)
|
||||
|
||||
// TODO: test the array row's field input values have been duplicated as well
|
||||
// TODO: test the array row has been duplicated
|
||||
|
||||
return { popupContentLocator, rowActionsButtonLocator }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { Locator, Page } from 'playwright'
|
||||
|
||||
import { expect } from 'playwright/test'
|
||||
|
||||
import { openArrayRowActions } from './openArrayRowActions.js'
|
||||
|
||||
/**
|
||||
@@ -14,9 +12,6 @@ export const removeArrayRow = async (
|
||||
popupContentLocator: Locator
|
||||
rowActionsButtonLocator: Locator
|
||||
}> => {
|
||||
const rowLocator = page.locator(`#field-${fieldName} .array-field__row`)
|
||||
const numberOfPrevRows = await rowLocator.count()
|
||||
|
||||
const { popupContentLocator, rowActionsButtonLocator } = await openArrayRowActions(page, {
|
||||
fieldName,
|
||||
rowIndex,
|
||||
@@ -24,10 +19,8 @@ export const removeArrayRow = async (
|
||||
|
||||
await popupContentLocator.locator('.array-actions__action.array-actions__remove').click()
|
||||
|
||||
expect(await rowLocator.count()).toBe(numberOfPrevRows - 1)
|
||||
|
||||
// TODO: test the array row has been removed in the _correct position_ (original row index)
|
||||
// another row may have been moved into its place, need to ensure the test accounts for this fact
|
||||
// TODO: test the array row has been removed
|
||||
// another row may have been moved into its place, though
|
||||
|
||||
return { popupContentLocator, rowActionsButtonLocator }
|
||||
}
|
||||
|
||||
@@ -1,30 +1,10 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import { exactText } from 'helpers.js'
|
||||
|
||||
import { openArrayRowActions } from '../array/openArrayRowActions.js'
|
||||
import { openBlocksDrawer } from './openBlocksDrawer.js'
|
||||
|
||||
const selectBlockFromDrawer = async ({
|
||||
blocksDrawer,
|
||||
blockToSelect,
|
||||
}: {
|
||||
blocksDrawer: Locator
|
||||
blockToSelect: string
|
||||
}) => {
|
||||
const blockCard = blocksDrawer.locator('.blocks-drawer__block .thumbnail-card__label', {
|
||||
hasText: blockToSelect,
|
||||
})
|
||||
|
||||
await expect(blockCard).toBeVisible()
|
||||
|
||||
await blocksDrawer.getByRole('button', { name: exactText(blockToSelect) }).click()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a block to the end of the blocks array using the primary "Add Block" button.
|
||||
*/
|
||||
export const addBlock = async ({
|
||||
page,
|
||||
fieldName = 'blocks',
|
||||
@@ -37,66 +17,15 @@ export const addBlock = async ({
|
||||
fieldName: string
|
||||
page: Page
|
||||
}) => {
|
||||
const rowLocator = page.locator(
|
||||
`#field-${fieldName} > .blocks-field__rows > div > .blocks-field__row`,
|
||||
)
|
||||
|
||||
const numberOfPrevRows = await rowLocator.count()
|
||||
|
||||
const blocksDrawer = await openBlocksDrawer({ page, fieldName })
|
||||
|
||||
await selectBlockFromDrawer({
|
||||
blocksDrawer,
|
||||
blockToSelect,
|
||||
const blockCard = blocksDrawer.locator('.blocks-drawer__block .thumbnail-card__label', {
|
||||
hasText: blockToSelect,
|
||||
})
|
||||
|
||||
await expect(rowLocator).toHaveCount(numberOfPrevRows + 1)
|
||||
await expect(blockCard).toBeVisible()
|
||||
|
||||
await blocksDrawer.getByRole('button', { name: exactText(blockToSelect) }).click()
|
||||
|
||||
// expect to see the block on the page
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `addBlock`, but inserts the block at the specified index using the row actions menu.
|
||||
*/
|
||||
export const addBlockBelow = async (
|
||||
page: Page,
|
||||
{
|
||||
fieldName = 'blocks',
|
||||
blockToSelect = 'Block',
|
||||
rowIndex = 0,
|
||||
}: {
|
||||
/**
|
||||
* The name of the block to select from the blocks drawer.
|
||||
*/
|
||||
blockToSelect: string
|
||||
fieldName: string
|
||||
/**
|
||||
* The index at which to insert the block.
|
||||
*/
|
||||
rowIndex?: number
|
||||
},
|
||||
) => {
|
||||
const rowLocator = page.locator(
|
||||
`#field-${fieldName} > .blocks-field__rows > div > .blocks-field__row`,
|
||||
)
|
||||
|
||||
const numberOfPrevRows = await rowLocator.count()
|
||||
|
||||
const { popupContentLocator, rowActionsButtonLocator } = await openArrayRowActions(page, {
|
||||
fieldName,
|
||||
rowIndex,
|
||||
})
|
||||
|
||||
await popupContentLocator.locator('.array-actions__action.array-actions__add').click()
|
||||
|
||||
const blocksDrawer = page.locator('[id^=drawer_1_blocks-drawer-]')
|
||||
|
||||
await selectBlockFromDrawer({
|
||||
blocksDrawer,
|
||||
blockToSelect,
|
||||
})
|
||||
|
||||
await expect(rowLocator).toHaveCount(numberOfPrevRows + 1)
|
||||
|
||||
return { popupContentLocator, rowActionsButtonLocator }
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { Locator, Page } from 'playwright'
|
||||
|
||||
import { expect } from 'playwright/test'
|
||||
|
||||
import { openArrayRowActions } from '../array/openArrayRowActions.js'
|
||||
|
||||
/**
|
||||
* Duplicates the block row at the specified index.
|
||||
*/
|
||||
export const duplicateBlock = async (
|
||||
page: Page,
|
||||
{ fieldName, rowIndex = 0 }: Parameters<typeof openArrayRowActions>[1],
|
||||
): Promise<{
|
||||
popupContentLocator: Locator
|
||||
rowActionsButtonLocator: Locator
|
||||
rowCount: number
|
||||
}> => {
|
||||
const rowLocator = page.locator(
|
||||
`#field-${fieldName} > .blocks-field__rows > div > .blocks-field__row`,
|
||||
)
|
||||
|
||||
const numberOfPrevRows = await rowLocator.count()
|
||||
|
||||
const { popupContentLocator, rowActionsButtonLocator } = await openArrayRowActions(page, {
|
||||
fieldName,
|
||||
rowIndex,
|
||||
})
|
||||
|
||||
await popupContentLocator.locator('.array-actions__action.array-actions__duplicate').click()
|
||||
const numberOfCurrentRows = await rowLocator.count()
|
||||
|
||||
expect(numberOfCurrentRows).toBe(numberOfPrevRows + 1)
|
||||
|
||||
// TODO: test the array row's field input values have been duplicated as well
|
||||
|
||||
return { popupContentLocator, rowActionsButtonLocator, rowCount: numberOfCurrentRows }
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
export { addBlock, addBlockBelow } from './addBlock.js'
|
||||
export { duplicateBlock } from './duplicateBlock.js'
|
||||
export { addBlock } from './addBlock.js'
|
||||
export { openBlocksDrawer } from './openBlocksDrawer.js'
|
||||
export { removeAllBlocks } from './removeAllBlocks.js'
|
||||
export { reorderBlocks } from './reorderBlocks.js'
|
||||
|
||||
@@ -17,6 +17,8 @@ import { LexicalLocalizedFields } from './collections/LexicalLocalized/index.js'
|
||||
import { LexicalMigrateFields } from './collections/LexicalMigrate/index.js'
|
||||
import { LexicalObjectReferenceBugCollection } from './collections/LexicalObjectReferenceBug/index.js'
|
||||
import { LexicalRelationshipsFields } from './collections/LexicalRelationships/index.js'
|
||||
import { OnDemandForm } from './collections/OnDemandForm/index.js'
|
||||
import { OnDemandOutsideForm } from './collections/OnDemandOutsideForm/index.js'
|
||||
import RichTextFields from './collections/RichText/index.js'
|
||||
import TextFields from './collections/Text/index.js'
|
||||
import Uploads from './collections/Upload/index.js'
|
||||
@@ -46,6 +48,8 @@ export const baseConfig: Partial<Config> = {
|
||||
TextFields,
|
||||
Uploads,
|
||||
ArrayFields,
|
||||
OnDemandForm,
|
||||
OnDemandOutsideForm,
|
||||
],
|
||||
globals: [TabsWithRichText],
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user