diff --git a/docs/cloud/projects.mdx b/docs/cloud/projects.mdx index 491c762272..2f4bb4ff3a 100644 --- a/docs/cloud/projects.mdx +++ b/docs/cloud/projects.mdx @@ -98,7 +98,7 @@ From there, you are ready to make updates to your project. When you are ready to Projects generated from a template will come pre-configured with the official Cloud Plugin, but if you are using your own repository you will need to add this into your project. To do so, add the plugin to your Payload Config: -`yarn add @payloadcms/plugin-cloud` +`yarn add @payloadcms/payload-cloud` ```js import { payloadCloudPlugin } from '@payloadcms/payload-cloud' @@ -115,6 +115,11 @@ export default buildConfig({ over Payload Cloud's email service. + + Good to know: the Payload Cloud Plugin was previously named `@payloadcms/plugin-cloud`. If you are + using this plugin, you should update to the new package name. + + #### **Optional configuration** If you wish to opt-out of any Payload cloud features, the plugin also accepts options to do so. diff --git a/package.json b/package.json index dfe608f1fb..92e961b12d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "payload-monorepo", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "private": true, "type": "module", "scripts": { diff --git a/packages/create-payload-app/package.json b/packages/create-payload-app/package.json index 826b10716a..f00db9117a 100644 --- a/packages/create-payload-app/package.json +++ b/packages/create-payload-app/package.json @@ -1,6 +1,6 @@ { "name": "create-payload-app", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "homepage": "https://payloadcms.com", "repository": { "type": "git", diff --git a/packages/db-mongodb/package.json b/packages/db-mongodb/package.json index 9ad00b0dbf..6c91da5e3e 100644 --- a/packages/db-mongodb/package.json +++ b/packages/db-mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/db-mongodb", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "The officially supported MongoDB database adapter for Payload", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/db-postgres/package.json b/packages/db-postgres/package.json index 6169108525..170a6eb51a 100644 --- a/packages/db-postgres/package.json +++ b/packages/db-postgres/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/db-postgres", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "The officially supported Postgres database adapter for Payload", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/db-sqlite/package.json b/packages/db-sqlite/package.json index cd7099a8d6..cd2e8aebad 100644 --- a/packages/db-sqlite/package.json +++ b/packages/db-sqlite/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/db-sqlite", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "The officially supported SQLite database adapter for Payload", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/db-vercel-postgres/package.json b/packages/db-vercel-postgres/package.json index 003861a36e..954dc56023 100644 --- a/packages/db-vercel-postgres/package.json +++ b/packages/db-vercel-postgres/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/db-vercel-postgres", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "Vercel Postgres adapter for Payload", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/drizzle/package.json b/packages/drizzle/package.json index d40a16c90d..61f4e76d29 100644 --- a/packages/drizzle/package.json +++ b/packages/drizzle/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/drizzle", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "A library of shared functions used by different payload database adapters", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/email-nodemailer/package.json b/packages/email-nodemailer/package.json index a9d6fb3f21..0aafef4098 100644 --- a/packages/email-nodemailer/package.json +++ b/packages/email-nodemailer/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/email-nodemailer", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "Payload Nodemailer Email Adapter", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/email-resend/package.json b/packages/email-resend/package.json index ea07cd06ef..4350f800dd 100644 --- a/packages/email-resend/package.json +++ b/packages/email-resend/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/email-resend", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "Payload Resend Email Adapter", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/graphql/package.json b/packages/graphql/package.json index efa5b8992e..cd578e6f7e 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/graphql", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "homepage": "https://payloadcms.com", "repository": { "type": "git", diff --git a/packages/live-preview-react/package.json b/packages/live-preview-react/package.json index d1027822e3..6b8d09ae8f 100644 --- a/packages/live-preview-react/package.json +++ b/packages/live-preview-react/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/live-preview-react", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "The official React SDK for Payload Live Preview", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/live-preview-vue/package.json b/packages/live-preview-vue/package.json index bec89e5cf1..e2d1234ac0 100644 --- a/packages/live-preview-vue/package.json +++ b/packages/live-preview-vue/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/live-preview-vue", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "The official Vue SDK for Payload Live Preview", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/live-preview/package.json b/packages/live-preview/package.json index c2b4ded2b1..a1abddcb2a 100644 --- a/packages/live-preview/package.json +++ b/packages/live-preview/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/live-preview", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "The official live preview JavaScript SDK for Payload", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/next/package.json b/packages/next/package.json index 754a0e96b1..306e7775bf 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/next", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "homepage": "https://payloadcms.com", "repository": { "type": "git", diff --git a/packages/payload-cloud/package.json b/packages/payload-cloud/package.json index d0804cabef..3bd7ac280c 100644 --- a/packages/payload-cloud/package.json +++ b/packages/payload-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/payload-cloud", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "The official Payload Cloud plugin", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/payload/package.json b/packages/payload/package.json index ca768dcee2..45f8b13932 100644 --- a/packages/payload/package.json +++ b/packages/payload/package.json @@ -1,6 +1,6 @@ { "name": "payload", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "Node, React, Headless CMS and Application Framework built on Next.js", "keywords": [ "admin panel", diff --git a/packages/plugin-cloud-storage/package.json b/packages/plugin-cloud-storage/package.json index 4191ff4494..19d7ed58eb 100644 --- a/packages/plugin-cloud-storage/package.json +++ b/packages/plugin-cloud-storage/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/plugin-cloud-storage", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "The official cloud storage plugin for Payload CMS", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/plugin-form-builder/package.json b/packages/plugin-form-builder/package.json index f6de0eb2be..a4eead795c 100644 --- a/packages/plugin-form-builder/package.json +++ b/packages/plugin-form-builder/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/plugin-form-builder", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "Form builder plugin for Payload CMS", "keywords": [ "payload", diff --git a/packages/plugin-nested-docs/package.json b/packages/plugin-nested-docs/package.json index e9ac70ea31..81f6eb3bd1 100644 --- a/packages/plugin-nested-docs/package.json +++ b/packages/plugin-nested-docs/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/plugin-nested-docs", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "The official Nested Docs plugin for Payload", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/plugin-redirects/package.json b/packages/plugin-redirects/package.json index 4b852565d6..bbca25a26d 100644 --- a/packages/plugin-redirects/package.json +++ b/packages/plugin-redirects/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/plugin-redirects", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "Redirects plugin for Payload", "keywords": [ "payload", diff --git a/packages/plugin-search/package.json b/packages/plugin-search/package.json index f01878eb90..2b5cdd3ae1 100644 --- a/packages/plugin-search/package.json +++ b/packages/plugin-search/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/plugin-search", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "Search plugin for Payload", "keywords": [ "payload", diff --git a/packages/plugin-sentry/package.json b/packages/plugin-sentry/package.json index c1e30cd754..92ce2ee698 100644 --- a/packages/plugin-sentry/package.json +++ b/packages/plugin-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/plugin-sentry", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "Sentry plugin for Payload", "keywords": [ "payload", diff --git a/packages/plugin-seo/package.json b/packages/plugin-seo/package.json index 6c2d6c3db5..152c11d3e1 100644 --- a/packages/plugin-seo/package.json +++ b/packages/plugin-seo/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/plugin-seo", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "SEO plugin for Payload", "keywords": [ "payload", diff --git a/packages/plugin-stripe/package.json b/packages/plugin-stripe/package.json index eb75cf5a53..2efc412574 100644 --- a/packages/plugin-stripe/package.json +++ b/packages/plugin-stripe/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/plugin-stripe", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "Stripe plugin for Payload", "keywords": [ "payload", diff --git a/packages/richtext-lexical/package.json b/packages/richtext-lexical/package.json index 49efafa8eb..9730dbc2d0 100644 --- a/packages/richtext-lexical/package.json +++ b/packages/richtext-lexical/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/richtext-lexical", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "The officially supported Lexical richtext adapter for Payload", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx b/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx index cd4b84923f..a3b62216b0 100644 --- a/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx +++ b/packages/richtext-lexical/src/features/blocks/client/plugin/index.tsx @@ -24,6 +24,7 @@ import { COMMAND_PRIORITY_EDITOR, type RangeSelection, } from 'lexical' +import { useLexicalDrawer } from 'packages/richtext-lexical/src/utilities/fieldsDrawer/useLexicalDrawer.js' import React, { useEffect, useState } from 'react' import type { PluginComponent } from '../../../typesClient.js' @@ -44,7 +45,6 @@ export type InsertBlockPayload = BlockFieldsOptionalID export const BlocksPlugin: PluginComponent = () => { const [editor] = useLexicalComposerContext() - const { closeModal, toggleModal } = useModal() const [blockFields, setBlockFields] = useState(null) const [blockType, setBlockType] = useState('' as any) const [targetNodeKey, setTargetNodeKey] = useState(null) @@ -58,6 +58,8 @@ export const BlocksPlugin: PluginComponent = () => { depth: editDepth, }) + const { toggleDrawer } = useLexicalDrawer(drawerSlug) + const { field: { richTextComponentMap }, } = useEditorConfigContext() @@ -135,7 +137,7 @@ export const BlocksPlugin: PluginComponent = () => { setBlockType(fields?.blockType ?? ('' as any)) if (nodeKey) { - toggleModal(drawerSlug) + toggleDrawer() return true } @@ -150,14 +152,14 @@ export const BlocksPlugin: PluginComponent = () => { if (rangeSelection) { //setLastSelection(rangeSelection) - toggleModal(drawerSlug) + toggleDrawer() } return true }, COMMAND_PRIORITY_EDITOR, ), ) - }, [editor, targetNodeKey, toggleModal, drawerSlug]) + }, [editor, targetNodeKey, toggleDrawer]) if (!blockFields) { return null @@ -192,7 +194,6 @@ export const BlocksPlugin: PluginComponent = () => { featureKey="blocks" fieldMapOverride={clientBlock?.fields} handleDrawerSubmit={(_fields, data) => { - closeModal(drawerSlug) if (!data) { return } diff --git a/packages/richtext-lexical/src/features/experimental_table/client/plugins/TablePlugin/index.tsx b/packages/richtext-lexical/src/features/experimental_table/client/plugins/TablePlugin/index.tsx index fd96b56c36..d61d2b9dbe 100644 --- a/packages/richtext-lexical/src/features/experimental_table/client/plugins/TablePlugin/index.tsx +++ b/packages/richtext-lexical/src/features/experimental_table/client/plugins/TablePlugin/index.tsx @@ -16,6 +16,7 @@ import { INSERT_TABLE_COMMAND, TableNode } from '@lexical/table' import { mergeRegister } from '@lexical/utils' import { formatDrawerSlug, useEditDepth, useModal } from '@payloadcms/ui' import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical' +import { useLexicalDrawer } from 'packages/richtext-lexical/src/utilities/fieldsDrawer/useLexicalDrawer.js' import { createContext, useContext, useEffect, useMemo, useState } from 'react' import * as React from 'react' @@ -83,7 +84,6 @@ export function TableContext({ children }: { children: JSX.Element }) { export const TablePlugin: PluginComponent = () => { const [editor] = useLexicalComposerContext() const cellContext = useContext(CellContext) - const { closeModal, toggleModal } = useModal() const editDepth = useEditDepth() const { uuid } = useEditorConfigContext() @@ -91,6 +91,7 @@ export const TablePlugin: PluginComponent = () => { slug: 'lexical-table-create-' + uuid, depth: editDepth, }) + const { toggleDrawer } = useLexicalDrawer(drawerSlug) useEffect(() => { if (!editor.hasNodes([TableNode])) { @@ -111,14 +112,14 @@ export const TablePlugin: PluginComponent = () => { }) if (rangeSelection) { - toggleModal(drawerSlug) + toggleDrawer() } return true }, COMMAND_PRIORITY_EDITOR, ), ) - }, [cellContext, drawerSlug, editor, toggleModal]) + }, [cellContext, editor, toggleDrawer]) return ( @@ -127,8 +128,6 @@ export const TablePlugin: PluginComponent = () => { drawerTitle="Create Table" featureKey="experimental_table" handleDrawerSubmit={(_fields, data) => { - closeModal(drawerSlug) - if (!data.columns || !data.rows) { return } diff --git a/packages/richtext-lexical/src/features/link/client/plugins/floatingLinkEditor/LinkEditor/index.tsx b/packages/richtext-lexical/src/features/link/client/plugins/floatingLinkEditor/LinkEditor/index.tsx index b218bc77cb..f3a686a2e9 100644 --- a/packages/richtext-lexical/src/features/link/client/plugins/floatingLinkEditor/LinkEditor/index.tsx +++ b/packages/richtext-lexical/src/features/link/client/plugins/floatingLinkEditor/LinkEditor/index.tsx @@ -33,6 +33,7 @@ import { useEditorConfigContext } from '../../../../../../lexical/config/client/ import { getSelectedNode } from '../../../../../../lexical/utils/getSelectedNode.js' import { setFloatingElemPositionForLinkEditor } from '../../../../../../lexical/utils/setFloatingElemPositionForLinkEditor.js' import { FieldsDrawer } from '../../../../../../utilities/fieldsDrawer/Drawer.js' +import { useLexicalDrawer } from '../../../../../../utilities/fieldsDrawer/useLexicalDrawer.js' import { $isAutoLinkNode } from '../../../../nodes/AutoLinkNode.js' import { $createLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND } from '../../../../nodes/LinkNode.js' import { TOGGLE_LINK_WITH_MODAL_COMMAND } from './commands.js' @@ -54,7 +55,6 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R ({ id?: string; text: string } & LinkFields) | undefined >() - const { closeModal, toggleModal } = useModal() const editDepth = useEditDepth() const [isLink, setIsLink] = useState(false) const [selectedNodes, setSelectedNodes] = useState([]) @@ -66,6 +66,8 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R depth: editDepth, }) + const { toggleDrawer } = useLexicalDrawer(drawerSlug) + const setNotLink = useCallback(() => { setIsLink(false) if (editorRef && editorRef.current) { @@ -204,14 +206,14 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R // Now, open the modal $updateLinkEditor() - toggleModal(drawerSlug) + toggleDrawer() return true }, COMMAND_PRIORITY_LOW, ), ) - }, [editor, $updateLinkEditor, toggleModal, drawerSlug]) + }, [editor, $updateLinkEditor, toggleDrawer, drawerSlug]) useEffect(() => { const scrollerElem = anchorElem.parentElement @@ -292,7 +294,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R aria-label="Edit link" className="link-edit" onClick={() => { - toggleModal(drawerSlug) + toggleDrawer() }} onMouseDown={(event) => { event.preventDefault() @@ -329,8 +331,6 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R drawerTitle={t('fields:editLink')} featureKey="link" handleDrawerSubmit={(fields: FormState, data: Data) => { - closeModal(drawerSlug) - const newLinkPayload = data as { text: string } & LinkFields const bareLinkFields: LinkFields = { diff --git a/packages/richtext-lexical/src/features/relationship/client/components/RelationshipComponent.tsx b/packages/richtext-lexical/src/features/relationship/client/components/RelationshipComponent.tsx index 9fef40dba9..bb8d294e7e 100644 --- a/packages/richtext-lexical/src/features/relationship/client/components/RelationshipComponent.tsx +++ b/packages/richtext-lexical/src/features/relationship/client/components/RelationshipComponent.tsx @@ -5,7 +5,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection.js' import { mergeRegister } from '@lexical/utils' import { getTranslation } from '@payloadcms/translations' -import { Button, useConfig, useDocumentDrawer, usePayloadAPI, useTranslation } from '@payloadcms/ui' +import { Button, useConfig, usePayloadAPI, useTranslation } from '@payloadcms/ui' import { $getNodeByKey, $getSelection, @@ -20,6 +20,7 @@ import React, { useCallback, useEffect, useReducer, useRef, useState } from 'rea import type { RelationshipData } from '../../server/nodes/RelationshipNode.js' import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js' +import { useLexicalDocumentDrawer } from '../../../../utilities/fieldsDrawer/useLexicalDocumentDrawer.js' import { INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND } from '../drawer/commands.js' import { $isRelationshipNode } from '../nodes/RelationshipNode.js' import './index.scss' @@ -73,7 +74,7 @@ const Component: React.FC = (props) => { { initialParams }, ) - const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer }] = useDocumentDrawer({ + const { closeDocumentDrawer, DocumentDrawer, DocumentDrawerToggler } = useLexicalDocumentDrawer({ id: value, collectionSlug: relatedCollection.slug, }) @@ -91,10 +92,10 @@ const Component: React.FC = (props) => { cacheBust, // do this to get the usePayloadAPI to re-fetch the data even though the URL string hasn't changed }) - closeDrawer() + closeDocumentDrawer() dispatchCacheBust() }, - [cacheBust, setParams, closeDrawer], + [cacheBust, setParams, closeDocumentDrawer], ) const $onDelete = useCallback( @@ -172,7 +173,7 @@ const Component: React.FC = (props) => { buttonStyle="icon-label" className={`${baseClass}__swapButton`} disabled={field?.admin?.readOnly} - el="div" + el="button" icon="swap" onClick={() => { if (nodeKey) { diff --git a/packages/richtext-lexical/src/features/relationship/client/drawer/index.tsx b/packages/richtext-lexical/src/features/relationship/client/drawer/index.tsx index ac640c16c3..2b6eae7cba 100644 --- a/packages/richtext-lexical/src/features/relationship/client/drawer/index.tsx +++ b/packages/richtext-lexical/src/features/relationship/client/drawer/index.tsx @@ -2,10 +2,10 @@ import type { LexicalEditor } from 'lexical' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js' -import { useListDrawer } from '@payloadcms/ui' import { $getNodeByKey, COMMAND_PRIORITY_EDITOR } from 'lexical' import React, { useCallback, useEffect, useState } from 'react' +import { useLexicalListDrawer } from '../../../../utilities/fieldsDrawer/useLexicalListDrawer.js' import { $createRelationshipNode } from '../nodes/RelationshipNode.js' import { INSERT_RELATIONSHIP_COMMAND } from '../plugins/index.js' import { EnabledRelationshipsCondition } from '../utils/EnabledRelationshipsCondition.js' @@ -48,8 +48,8 @@ const RelationshipDrawerComponent: React.FC = ({ enabledCollectionSlugs } ) const [replaceNodeKey, setReplaceNodeKey] = useState(null) - const [ListDrawer, ListDrawerToggler, { closeDrawer, isDrawerOpen, openDrawer }] = useListDrawer({ - collectionSlugs: enabledCollectionSlugs!, + const { closeListDrawer, isListDrawerOpen, ListDrawer, openListDrawer } = useLexicalListDrawer({ + collectionSlugs: enabledCollectionSlugs ? enabledCollectionSlugs : undefined, selectedCollection: selectedCollectionSlug, }) @@ -60,12 +60,12 @@ const RelationshipDrawerComponent: React.FC = ({ enabledCollectionSlugs } INSERT_RELATIONSHIP_WITH_DRAWER_COMMAND, (payload) => { setReplaceNodeKey(payload?.replace ? payload?.replace.nodeKey : null) - openDrawer() + openListDrawer() return true }, COMMAND_PRIORITY_EDITOR, ) - }, [editor, openDrawer]) + }, [editor, openListDrawer]) const onSelect = useCallback( ({ collectionSlug, docID }) => { @@ -75,16 +75,16 @@ const RelationshipDrawerComponent: React.FC = ({ enabledCollectionSlugs } replaceNodeKey, value: docID, }) - closeDrawer() + closeListDrawer() }, - [editor, closeDrawer, replaceNodeKey], + [editor, closeListDrawer, replaceNodeKey], ) useEffect(() => { // always reset back to first option // TODO: this is not working, see the ListDrawer component setSelectedCollectionSlug(enabledCollectionSlugs?.[0]) - }, [isDrawerOpen, enabledCollectionSlugs]) + }, [isListDrawerOpen, enabledCollectionSlugs]) return } diff --git a/packages/richtext-lexical/src/features/upload/client/component/index.tsx b/packages/richtext-lexical/src/features/upload/client/component/index.tsx index 61ed73205a..155080b82d 100644 --- a/packages/richtext-lexical/src/features/upload/client/component/index.tsx +++ b/packages/richtext-lexical/src/features/upload/client/component/index.tsx @@ -1,4 +1,5 @@ 'use client' +import type { BaseSelection } from 'lexical' import type { ClientCollectionConfig, Data } from 'payload' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js' @@ -7,13 +8,10 @@ import { mergeRegister } from '@lexical/utils' import { getTranslation } from '@payloadcms/translations' import { Button, - DrawerToggler, File, formatDrawerSlug, useConfig, - useDocumentDrawer, useEditDepth, - useModal, usePayloadAPI, useTranslation, } from '@payloadcms/ui' @@ -35,6 +33,8 @@ import type { UploadNode } from '../nodes/UploadNode.js' import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js' import { FieldsDrawer } from '../../../../utilities/fieldsDrawer/Drawer.js' +import { useLexicalDocumentDrawer } from '../../../../utilities/fieldsDrawer/useLexicalDocumentDrawer.js' +import { useLexicalDrawer } from '../../../../utilities/fieldsDrawer/useLexicalDrawer.js' import { EnabledRelationshipsCondition } from '../../../relationship/client/utils/EnabledRelationshipsCondition.js' import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from '../drawer/commands.js' import { $isUploadNode } from '../nodes/UploadNode.js' @@ -71,7 +71,6 @@ const Component: React.FC = (props) => { }, } = useConfig() const uploadRef = useRef(null) - const { closeModal } = useModal() const { uuid } = useEditorConfigContext() const editDepth = useEditDepth() const [editor] = useLexicalComposerContext() @@ -87,12 +86,15 @@ const Component: React.FC = (props) => { const componentID = useId() - const drawerSlug = formatDrawerSlug({ + const extraFieldsDrawerSlug = formatDrawerSlug({ slug: `lexical-upload-drawer-` + uuid + componentID, // There can be multiple upload components, each with their own drawer, in one single editor => separate them by componentID depth: editDepth, }) - const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer }] = useDocumentDrawer({ + // Need to use hook to initialize useEffect that restores cursor position + const { toggleDrawer } = useLexicalDrawer(extraFieldsDrawerSlug, true) + + const { closeDocumentDrawer, DocumentDrawer, DocumentDrawerToggler } = useLexicalDocumentDrawer({ id: value, collectionSlug: relatedCollection.slug, }) @@ -119,9 +121,9 @@ const Component: React.FC = (props) => { }) dispatchCacheBust() - closeDrawer() + closeDocumentDrawer() }, - [setParams, cacheBust, closeDrawer], + [setParams, cacheBust, closeDocumentDrawer], ) const $onDelete = useCallback( @@ -191,10 +193,8 @@ const Component: React.FC = (props) => { uploadNode.setData(newData) } }) - - closeModal(drawerSlug) }, - [closeModal, editor, drawerSlug, nodeKey], + [editor, nodeKey], ) return ( @@ -225,28 +225,25 @@ const Component: React.FC = (props) => { {editor.isEditable() && ( {hasExtraFields ? ( - - { - e.preventDefault() - }} - round - tooltip={t('fields:editRelationship')} - /> - + el="button" + icon="edit" + onClick={() => { + toggleDrawer() + }} + round + tooltip={t('fields:editRelationship')} + /> ) : null} { editor.dispatchCommand(INSERT_UPLOAD_WITH_DRAWER_COMMAND, { @@ -282,7 +279,7 @@ const Component: React.FC = (props) => { {hasExtraFields ? ( = ({ enabledCollectionSlugs }) => { const [replaceNodeKey, setReplaceNodeKey] = useState(null) - const [ListDrawer, ListDrawerToggler, { closeDrawer, openDrawer }] = useListDrawer({ + const { closeListDrawer, ListDrawer, openListDrawer } = useLexicalListDrawer({ collectionSlugs: enabledCollectionSlugs, uploads: true, }) @@ -69,24 +69,24 @@ const UploadDrawerComponent: React.FC = ({ enabledCollectionSlugs }) => { INSERT_UPLOAD_WITH_DRAWER_COMMAND, (payload) => { setReplaceNodeKey(payload?.replace ? payload?.replace.nodeKey : null) - openDrawer() + openListDrawer() return true }, COMMAND_PRIORITY_EDITOR, ) - }, [editor, openDrawer]) + }, [editor, openListDrawer]) const onSelect = useCallback( ({ collectionSlug, docID }) => { + closeListDrawer() insertUpload({ editor, relationTo: collectionSlug, replaceNodeKey, value: docID, }) - closeDrawer() }, - [editor, closeDrawer, replaceNodeKey], + [editor, closeListDrawer, replaceNodeKey], ) return diff --git a/packages/richtext-lexical/src/utilities/fieldsDrawer/Drawer.tsx b/packages/richtext-lexical/src/utilities/fieldsDrawer/Drawer.tsx index 3dfac9358d..97ca2bdfb7 100644 --- a/packages/richtext-lexical/src/utilities/fieldsDrawer/Drawer.tsx +++ b/packages/richtext-lexical/src/utilities/fieldsDrawer/Drawer.tsx @@ -1,7 +1,7 @@ 'use client' import type { ClientField, Data, FormState, JsonObject } from 'payload' -import { Drawer } from '@payloadcms/ui' +import { Drawer, useModal } from '@payloadcms/ui' import React from 'react' import { DrawerContent } from './DrawerContent.js' @@ -36,6 +36,7 @@ export const FieldsDrawer: React.FC = ({ schemaPath, schemaPathSuffix, }) => { + const { closeModal } = useModal() // The Drawer only renders its children (and itself) if it's open. Thus, by extracting the main content // to DrawerContent, this should be faster return ( @@ -44,7 +45,19 @@ export const FieldsDrawer: React.FC = ({ data={data} featureKey={featureKey} fieldMapOverride={fieldMapOverride} - handleDrawerSubmit={handleDrawerSubmit} + handleDrawerSubmit={(args, args2) => { + // Simply close drawer - no need for useLexicalDrawer here as at this point, + // we don't need to restore the cursor position. This is handled by the useEffect in useLexicalDrawer. + closeModal(drawerSlug) + + // Actual drawer submit logic needs to be triggered after the drawer is closed. + // That's because the lexical selection / cursor restore logic that is striggerer by + // `useLexicalDrawer` neeeds to be triggered before any editor.update calls that may happen + // in the `handleDrawerSubmit` function. + setTimeout(() => { + handleDrawerSubmit(args, args2) + }, 1) + }} schemaFieldsPathOverride={schemaFieldsPathOverride} schemaPath={schemaPath} schemaPathSuffix={schemaPathSuffix} diff --git a/packages/richtext-lexical/src/utilities/fieldsDrawer/useLexicalDocumentDrawer.tsx b/packages/richtext-lexical/src/utilities/fieldsDrawer/useLexicalDocumentDrawer.tsx new file mode 100644 index 0000000000..6f0e92096f --- /dev/null +++ b/packages/richtext-lexical/src/utilities/fieldsDrawer/useLexicalDocumentDrawer.tsx @@ -0,0 +1,81 @@ +'use client' +import type { UseDocumentDrawer } from '@payloadcms/ui' +import type { BaseSelection } from 'lexical' + +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { useDocumentDrawer, useModal } from '@payloadcms/ui' +import { $getPreviousSelection, $getSelection, $setSelection } from 'lexical' +import { useCallback, useEffect, useState } from 'react' + +/** + * + * Wrapper around useDocumentDrawer that restores and saves selection state (cursor position) when opening and closing the drawer. + * By default, the lexical cursor position may be lost when opening a drawer and clicking somewhere on that drawer. + */ +export const useLexicalDocumentDrawer = ( + args: Parameters[0], +): { + closeDocumentDrawer: () => void + DocumentDrawer: ReturnType[0] + documentDrawerSlug: string + DocumentDrawerToggler: ReturnType[1] +} => { + const [editor] = useLexicalComposerContext() + const [selectionState, setSelectionState] = useState(null) + const [wasOpen, setWasOpen] = useState(false) + + const [ + DocumentDrawer, + DocumentDrawerToggler, + { closeDrawer: closeDrawer, drawerSlug: documentDrawerSlug }, + ] = useDocumentDrawer(args) + const { modalState } = useModal() + + const storeSelection = useCallback(() => { + editor.read(() => { + const selection = $getSelection() ?? $getPreviousSelection() + setSelectionState(selection) + }) + setWasOpen(true) + }, [editor]) + + const restoreSelection = useCallback(() => { + if (selectionState) { + editor.update( + () => { + $setSelection(selectionState.clone()) + }, + { discrete: true, skipTransforms: true }, + ) + } + }, [editor, selectionState]) + + const closeDocumentDrawer = () => { + //restoreSelection() // Should already be stored by the useEffect below + closeDrawer() + } + + // We need to handle drawer closing via a useEffect, as toggleDrawer / closeDrawer will not be triggered if the drawer + // is closed by clicking outside of the drawer. This useEffect will handle everything. + useEffect(() => { + if (!wasOpen) { + return + } + + const thisModalState = modalState[documentDrawerSlug] + // Exists in modalState (thus has opened at least once before) and is closed + if (thisModalState && !thisModalState?.isOpen) { + setWasOpen(false) + setTimeout(() => { + restoreSelection() + }, 1) + } + }, [modalState, documentDrawerSlug, restoreSelection, wasOpen]) + + return { + closeDocumentDrawer, + DocumentDrawer, + documentDrawerSlug, + DocumentDrawerToggler: (props) => , + } +} diff --git a/packages/richtext-lexical/src/utilities/fieldsDrawer/useLexicalDrawer.tsx b/packages/richtext-lexical/src/utilities/fieldsDrawer/useLexicalDrawer.tsx new file mode 100644 index 0000000000..56388708c3 --- /dev/null +++ b/packages/richtext-lexical/src/utilities/fieldsDrawer/useLexicalDrawer.tsx @@ -0,0 +1,86 @@ +'use client' +import type { BaseSelection } from 'lexical' + +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { useModal } from '@payloadcms/ui' +import { $getPreviousSelection, $getSelection, $setSelection } from 'lexical' +import { useCallback, useEffect, useState } from 'react' + +/** + * + * Wrapper around useModal that restores and saves selection state (cursor position) when opening and closing the drawer. + * By default, the lexical cursor position may be lost when opening a drawer and clicking somewhere on that drawer. + */ +export const useLexicalDrawer = (slug: string, restoreLate?: boolean) => { + const [editor] = useLexicalComposerContext() + const [selectionState, setSelectionState] = useState(null) + const [wasOpen, setWasOpen] = useState(false) + + const { + closeModal: closeBaseModal, + isModalOpen: isBaseModalOpen, + modalState, + toggleModal: toggleBaseModal, + } = useModal() + + const storeSelection = useCallback(() => { + editor.read(() => { + const selection = $getSelection() ?? $getPreviousSelection() + setSelectionState(selection) + }) + }, [editor]) + + const restoreSelection = useCallback(() => { + if (selectionState) { + editor.update( + () => { + $setSelection(selectionState.clone()) + }, + { discrete: true, skipTransforms: true }, + ) + } + }, [editor, selectionState]) + + const closeDrawer = () => { + //restoreSelection() // Should already be stored by the useEffect below + closeBaseModal(slug) + } + + const toggleDrawer = () => { + if (!isBaseModalOpen(slug)) { + storeSelection() + } else { + restoreSelection() + } + setWasOpen(true) + toggleBaseModal(slug) + } + + // We need to handle drawer closing via a useEffect, as toggleDrawer / closeDrawer will not be triggered if the drawer + // is closed by clicking outside of the drawer. This useEffect will handle everything. + useEffect(() => { + if (!wasOpen) { + return + } + + const thisModalState = modalState[slug] + // Exists in modalState (thus has opened at least once before) and is closed + if (thisModalState && !thisModalState?.isOpen) { + setWasOpen(false) + + if (restoreLate) { + // restoreLate is used for upload extra field drawers. For some reason, the selection is not restored if we call restoreSelection immediately. + setTimeout(() => { + restoreSelection() + }, 1) + } else { + restoreSelection() + } + } + }, [modalState, slug, restoreSelection, wasOpen, restoreLate]) + + return { + closeDrawer, + toggleDrawer, + } +} diff --git a/packages/richtext-lexical/src/utilities/fieldsDrawer/useLexicalListDrawer.tsx b/packages/richtext-lexical/src/utilities/fieldsDrawer/useLexicalListDrawer.tsx new file mode 100644 index 0000000000..157274b78e --- /dev/null +++ b/packages/richtext-lexical/src/utilities/fieldsDrawer/useLexicalListDrawer.tsx @@ -0,0 +1,100 @@ +'use client' +import type { UseListDrawer } from '@payloadcms/ui' +import type { BaseSelection } from 'lexical' + +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { useListDrawer, useModal } from '@payloadcms/ui' +import { $getPreviousSelection, $getSelection, $setSelection } from 'lexical' +import { useCallback, useEffect, useState } from 'react' + +/** + * + * Wrapper around useListDrawer that restores and saves selection state (cursor position) when opening and closing the drawer. + * By default, the lexical cursor position may be lost when opening a drawer and clicking somewhere on that drawer. + */ +export const useLexicalListDrawer = ( + args: Parameters[0], +): { + closeListDrawer: () => void + isListDrawerOpen: boolean + ListDrawer: ReturnType[0] + listDrawerSlug: string + ListDrawerToggler: ReturnType[1] + openListDrawer: (selection?: BaseSelection) => void +} => { + const [editor] = useLexicalComposerContext() + const [selectionState, setSelectionState] = useState(null) + const [wasOpen, setWasOpen] = useState(false) + + const [ + BaseListDrawer, + BaseListDrawerToggler, + { + closeDrawer: baseCloseDrawer, + drawerSlug: listDrawerSlug, + isDrawerOpen, + openDrawer: baseOpenDrawer, + }, + ] = useListDrawer(args) + const { modalState } = useModal() + + const $storeSelection = useCallback(() => { + // editor.read() causes an error here when creating a new upload node from the slash menu. It seems like we can omit it here though, as all + // invocations of that functions are wrapped in editor.read() or editor.update() somewhere in the call stack. + const selection = $getSelection() ?? $getPreviousSelection() + setSelectionState(selection) + }, []) + + const restoreSelection = useCallback(() => { + if (selectionState) { + editor.update( + () => { + $setSelection(selectionState.clone()) + }, + { discrete: true, skipTransforms: true }, + ) + } + }, [editor, selectionState]) + + const closeListDrawer = () => { + //restoreSelection() // Should already be stored by the useEffect below + baseCloseDrawer() + } + + // We need to handle drawer closing via a useEffect, as toggleDrawer / closeDrawer will not be triggered if the drawer + // is closed by clicking outside of the drawer. This useEffect will handle everything. + useEffect(() => { + if (!wasOpen) { + return + } + + const thisModalState = modalState[listDrawerSlug] + // Exists in modalState (thus has opened at least once before) and is closed + if (thisModalState && !thisModalState?.isOpen) { + setWasOpen(false) + setTimeout(() => { + restoreSelection() + }, 1) + } + }, [modalState, listDrawerSlug, restoreSelection, wasOpen]) + + return { + closeListDrawer, + isListDrawerOpen: isDrawerOpen, + ListDrawer: BaseListDrawer, + listDrawerSlug, + ListDrawerToggler: (props) => ( + { + $storeSelection() + }} + /> + ), + openListDrawer: () => { + $storeSelection() + baseOpenDrawer() + setWasOpen(true) + }, + } +} diff --git a/packages/richtext-slate/package.json b/packages/richtext-slate/package.json index e7a79edd5b..e6c04fa684 100644 --- a/packages/richtext-slate/package.json +++ b/packages/richtext-slate/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/richtext-slate", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "The officially supported Slate richtext adapter for Payload", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/storage-azure/package.json b/packages/storage-azure/package.json index f37968b3a2..152cb4fb6c 100644 --- a/packages/storage-azure/package.json +++ b/packages/storage-azure/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/storage-azure", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "Payload storage adapter for Azure Blob Storage", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/storage-gcs/package.json b/packages/storage-gcs/package.json index 95ecd9d009..03918276fe 100644 --- a/packages/storage-gcs/package.json +++ b/packages/storage-gcs/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/storage-gcs", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "Payload storage adapter for Google Cloud Storage", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/storage-s3/package.json b/packages/storage-s3/package.json index 70e53563b1..e4b2ad61b5 100644 --- a/packages/storage-s3/package.json +++ b/packages/storage-s3/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/storage-s3", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "Payload storage adapter for Amazon S3", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/storage-uploadthing/package.json b/packages/storage-uploadthing/package.json index 24ab762b82..119bb5145c 100644 --- a/packages/storage-uploadthing/package.json +++ b/packages/storage-uploadthing/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/storage-uploadthing", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "Payload storage adapter for uploadthing", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/storage-vercel-blob/package.json b/packages/storage-vercel-blob/package.json index 1e9e7f1eed..3a3c73e086 100644 --- a/packages/storage-vercel-blob/package.json +++ b/packages/storage-vercel-blob/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/storage-vercel-blob", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "description": "Payload storage adapter for Vercel Blob Storage", "homepage": "https://payloadcms.com", "repository": { diff --git a/packages/translations/package.json b/packages/translations/package.json index 7cfe6e6ab7..fb0351964d 100644 --- a/packages/translations/package.json +++ b/packages/translations/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/translations", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "homepage": "https://payloadcms.com", "repository": { "type": "git", diff --git a/packages/ui/package.json b/packages/ui/package.json index e529846ff8..74506d6f9a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/ui", - "version": "3.0.0-beta.118", + "version": "3.0.0-beta.119", "homepage": "https://payloadcms.com", "repository": { "type": "git", diff --git a/packages/ui/src/elements/Button/index.scss b/packages/ui/src/elements/Button/index.scss index e9fc454020..403d54b0f0 100644 --- a/packages/ui/src/elements/Button/index.scss +++ b/packages/ui/src/elements/Button/index.scss @@ -13,6 +13,10 @@ } .btn { + * { + pointer-events: none; + } + // colors &--style-primary { --color: var(--theme-elevation-0); diff --git a/packages/ui/src/elements/DocumentDrawer/index.tsx b/packages/ui/src/elements/DocumentDrawer/index.tsx index 735607e7e4..f0d9f29e90 100644 --- a/packages/ui/src/elements/DocumentDrawer/index.tsx +++ b/packages/ui/src/elements/DocumentDrawer/index.tsx @@ -32,6 +32,7 @@ export const DocumentDrawerToggler: React.FC = ({ collectionSlug, disabled, drawerSlug, + onClick, ...rest }) => { const { t } = useTranslation() @@ -44,6 +45,7 @@ export const DocumentDrawerToggler: React.FC = ({ })} className={[className, `${baseClass}__toggler`].filter(Boolean).join(' ')} disabled={disabled} + onClick={onClick} slug={drawerSlug} {...rest} > diff --git a/packages/ui/src/elements/DocumentDrawer/types.ts b/packages/ui/src/elements/DocumentDrawer/types.ts index 5337c7148f..443155d8c0 100644 --- a/packages/ui/src/elements/DocumentDrawer/types.ts +++ b/packages/ui/src/elements/DocumentDrawer/types.ts @@ -27,6 +27,7 @@ export type DocumentTogglerProps = { readonly disabled?: boolean readonly drawerSlug?: string readonly id?: string + readonly onClick?: () => void } & Readonly> export type UseDocumentDrawer = (args: { collectionSlug: string; id?: number | string }) => [ diff --git a/packages/ui/src/elements/ListDrawer/index.tsx b/packages/ui/src/elements/ListDrawer/index.tsx index b9eab00e20..9374c8ec52 100644 --- a/packages/ui/src/elements/ListDrawer/index.tsx +++ b/packages/ui/src/elements/ListDrawer/index.tsx @@ -27,12 +27,14 @@ export const ListDrawerToggler: React.FC = ({ className, disabled, drawerSlug, + onClick, ...rest }) => { return ( diff --git a/packages/ui/src/elements/ListDrawer/types.ts b/packages/ui/src/elements/ListDrawer/types.ts index a282d98b96..8fed72f631 100644 --- a/packages/ui/src/elements/ListDrawer/types.ts +++ b/packages/ui/src/elements/ListDrawer/types.ts @@ -35,7 +35,7 @@ export type UseListDrawer = (args: { React.FC< Pick >, // drawer - React.FC>, // toggler + React.FC>, // toggler { closeDrawer: () => void collectionSlugs: SanitizedCollectionConfig['slug'][] diff --git a/packages/ui/src/exports/client/index.ts b/packages/ui/src/exports/client/index.ts index 1b4d83a0cb..cf359147fc 100644 --- a/packages/ui/src/exports/client/index.ts +++ b/packages/ui/src/exports/client/index.ts @@ -42,6 +42,12 @@ export { DeleteMany } from '../../elements/DeleteMany/index.js' export { DocumentControls } from '../../elements/DocumentControls/index.js' export { Dropzone } from '../../elements/Dropzone/index.js' export { useDocumentDrawer } from '../../elements/DocumentDrawer/index.js' +export type { + DocumentDrawerProps, + DocumentTogglerProps, + UseDocumentDrawer, +} from '../../elements/DocumentDrawer/types.js' + export { DocumentFields } from '../../elements/DocumentFields/index.js' export { Drawer, DrawerToggler, formatDrawerSlug } from '../../elements/Drawer/index.js' export { useDrawerSlug } from '../../elements/Drawer/useDrawerSlug.js' @@ -55,6 +61,11 @@ export { HydrateAuthProvider } from '../../elements/HydrateAuthProvider/index.js export { Locked } from '../../elements/Locked/index.js' export { ListControls } from '../../elements/ListControls/index.js' export { useListDrawer } from '../../elements/ListDrawer/index.js' +export type { + ListDrawerProps, + ListTogglerProps, + UseListDrawer, +} from '../../elements/ListDrawer/types.js' export { ListSelection } from '../../elements/ListSelection/index.js' export { ListHeader } from '../../elements/ListHeader/index.js' export { LoadingOverlayToggle } from '../../elements/Loading/index.js' diff --git a/templates/_template/package.json b/templates/_template/package.json index df51e89a6a..e0f51f0d2d 100644 --- a/templates/_template/package.json +++ b/templates/_template/package.json @@ -17,7 +17,7 @@ "dependencies": { "@payloadcms/db-mongodb": "beta", "@payloadcms/next": "beta", - "@payloadcms/plugin-cloud": "beta", + "@payloadcms/payload-cloud": "beta", "@payloadcms/richtext-lexical": "beta", "cross-env": "^7.0.3", "graphql": "^16.8.1", diff --git a/templates/blank/package.json b/templates/blank/package.json index df51e89a6a..e0f51f0d2d 100644 --- a/templates/blank/package.json +++ b/templates/blank/package.json @@ -17,7 +17,7 @@ "dependencies": { "@payloadcms/db-mongodb": "beta", "@payloadcms/next": "beta", - "@payloadcms/plugin-cloud": "beta", + "@payloadcms/payload-cloud": "beta", "@payloadcms/richtext-lexical": "beta", "cross-env": "^7.0.3", "graphql": "^16.8.1", diff --git a/templates/website/README.md b/templates/website/README.md index a9266903b6..5532fdb038 100644 --- a/templates/website/README.md +++ b/templates/website/README.md @@ -165,7 +165,7 @@ Core features: ### Cache -Although Next.js includes a robust set of caching strategies out of the box, Payload Cloud proxies and caches all files through Cloudflare using the [Official Cloud Plugin](https://github.com/payloadcms/plugin-cloud). This means that Next.js caching is not needed and is disabled by default. If you are hosting your app outside of Payload Cloud, you can easily reenable the Next.js caching mechanisms by removing the `no-store` directive from all fetch requests in `./src/app/_api` and then removing all instances of `export const dynamic = 'force-dynamic'` from pages files, such as `./src/app/(pages)/[slug]/page.tsx`. For more details, see the official [Next.js Caching Docs](https://nextjs.org/docs/app/building-your-application/caching). +Although Next.js includes a robust set of caching strategies out of the box, Payload Cloud proxies and caches all files through Cloudflare using the [Official Cloud Plugin](https://www.npmjs.com/package/@payloadcms/payload-cloud). This means that Next.js caching is not needed and is disabled by default. If you are hosting your app outside of Payload Cloud, you can easily reenable the Next.js caching mechanisms by removing the `no-store` directive from all fetch requests in `./src/app/_api` and then removing all instances of `export const dynamic = 'force-dynamic'` from pages files, such as `./src/app/(pages)/[slug]/page.tsx`. For more details, see the official [Next.js Caching Docs](https://nextjs.org/docs/app/building-your-application/caching). ## Development diff --git a/templates/website/package.json b/templates/website/package.json index 9997959ae7..00a7379fc3 100644 --- a/templates/website/package.json +++ b/templates/website/package.json @@ -21,7 +21,7 @@ "@payloadcms/db-mongodb": "beta", "@payloadcms/live-preview-react": "beta", "@payloadcms/next": "beta", - "@payloadcms/plugin-cloud": "beta", + "@payloadcms/payload-cloud": "beta", "@payloadcms/plugin-form-builder": "beta", "@payloadcms/plugin-nested-docs": "beta", "@payloadcms/plugin-redirects": "beta", diff --git a/templates/website/src/payload.config.ts b/templates/website/src/payload.config.ts index 8cb6e123f9..ff570006e8 100644 --- a/templates/website/src/payload.config.ts +++ b/templates/website/src/payload.config.ts @@ -1,7 +1,7 @@ // storage-adapter-import-placeholder import { mongooseAdapter } from '@payloadcms/db-mongodb' // database-adapter-import -import { payloadCloudPlugin } from '@payloadcms/plugin-cloud' +import { payloadCloudPlugin } from '@payloadcms/payload-cloud' import { formBuilderPlugin } from '@payloadcms/plugin-form-builder' import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs' import { redirectsPlugin } from '@payloadcms/plugin-redirects' diff --git a/templates/with-payload-cloud/package.json b/templates/with-payload-cloud/package.json index 2389dcbfd0..de829d5ba4 100644 --- a/templates/with-payload-cloud/package.json +++ b/templates/with-payload-cloud/package.json @@ -17,7 +17,7 @@ "dependencies": { "@payloadcms/db-mongodb": "beta", "@payloadcms/next": "beta", - "@payloadcms/plugin-cloud": "beta", + "@payloadcms/payload-cloud": "beta", "@payloadcms/richtext-lexical": "beta", "cross-env": "^7.0.3", "graphql": "^16.8.1", diff --git a/templates/with-payload-cloud/src/payload.config.ts b/templates/with-payload-cloud/src/payload.config.ts index 3476e669e8..aa4af8ff32 100644 --- a/templates/with-payload-cloud/src/payload.config.ts +++ b/templates/with-payload-cloud/src/payload.config.ts @@ -1,4 +1,4 @@ -import { payloadCloudPlugin } from '@payloadcms/plugin-cloud' +import { payloadCloudPlugin } from '@payloadcms/payload-cloud' import { mongooseAdapter } from '@payloadcms/db-mongodb' import { lexicalEditor } from '@payloadcms/richtext-lexical' import path from 'path' diff --git a/templates/with-postgres/package.json b/templates/with-postgres/package.json index 534baea1f6..dcbe60e2fc 100644 --- a/templates/with-postgres/package.json +++ b/templates/with-postgres/package.json @@ -18,7 +18,7 @@ "dependencies": { "@payloadcms/db-postgres": "beta", "@payloadcms/next": "beta", - "@payloadcms/plugin-cloud": "beta", + "@payloadcms/payload-cloud": "beta", "@payloadcms/richtext-lexical": "beta", "cross-env": "^7.0.3", "graphql": "^16.8.1", diff --git a/templates/with-vercel-mongodb/package.json b/templates/with-vercel-mongodb/package.json index f1f2fefb51..c0a436af9f 100644 --- a/templates/with-vercel-mongodb/package.json +++ b/templates/with-vercel-mongodb/package.json @@ -17,7 +17,7 @@ "dependencies": { "@payloadcms/db-mongodb": "beta", "@payloadcms/next": "beta", - "@payloadcms/plugin-cloud": "beta", + "@payloadcms/payload-cloud": "beta", "@payloadcms/richtext-lexical": "beta", "@payloadcms/storage-vercel-blob": "beta", "cross-env": "^7.0.3", diff --git a/templates/with-vercel-postgres/package.json b/templates/with-vercel-postgres/package.json index d852ba3762..0c3ff07c02 100644 --- a/templates/with-vercel-postgres/package.json +++ b/templates/with-vercel-postgres/package.json @@ -18,7 +18,7 @@ "dependencies": { "@payloadcms/db-vercel-postgres": "beta", "@payloadcms/next": "beta", - "@payloadcms/plugin-cloud": "beta", + "@payloadcms/payload-cloud": "beta", "@payloadcms/richtext-lexical": "beta", "@payloadcms/storage-vercel-blob": "beta", "cross-env": "^7.0.3", diff --git a/test/fields/collections/Lexical/e2e/main/e2e.spec.ts b/test/fields/collections/Lexical/e2e/main/e2e.spec.ts index 1e51cd964e..4ee8c72a20 100644 --- a/test/fields/collections/Lexical/e2e/main/e2e.spec.ts +++ b/test/fields/collections/Lexical/e2e/main/e2e.spec.ts @@ -1,3 +1,4 @@ +import type { SerializedLinkNode, SerializedUploadNode } from '@payloadcms/richtext-lexical' import type { BrowserContext, Page } from '@playwright/test' import type { SerializedEditorState, SerializedParagraphNode, SerializedTextNode } from 'lexical' @@ -685,6 +686,216 @@ describe('lexicalMain', () => { }) }) + test('creating a link, then clicking in the link drawer, then saving the link, should preserve cursor position and not move cursor to beginning of richtext field', async () => { + await navigateToLexicalFields() + const richTextField = page.locator('.rich-text-lexical').first() + await richTextField.scrollIntoViewIfNeeded() + await expect(richTextField).toBeVisible() + + const paragraph = richTextField.locator('.LexicalEditorTheme__paragraph').first() + await paragraph.scrollIntoViewIfNeeded() + await expect(paragraph).toBeVisible() + + /** + * Type some text + */ + await paragraph.click() + await page.keyboard.type('Some Text') + + await page.keyboard.press('Enter') + await page.keyboard.type('Hello there') + + // Select "there" by pressing shift + arrow left + for (let i = 0; i < 5; i++) { + await page.keyboard.press('Shift+ArrowLeft') + } + // Ensure inline toolbar appeared + const inlineToolbar = page.locator('.inline-toolbar-popup') + await expect(inlineToolbar).toBeVisible() + + const linkButton = inlineToolbar.locator('.toolbar-popup__button-link') + await expect(linkButton).toBeVisible() + await linkButton.click() + + /** + * Link Drawer + */ + + const linkDrawer = page.locator('dialog[id^=drawer_1_lexical-rich-text-link-]').first() // IDs starting with drawer_1_lexical-rich-text-link- (there's some other symbol after the underscore) + await expect(linkDrawer).toBeVisible() + await wait(500) + + const urlInput = linkDrawer.locator('#field-url').first() + // Click on the input to focus it + await urlInput.click() + // should be https:// value + await expect(urlInput).toHaveValue('https://') + // Change it to https://google.com + await urlInput.fill('https://google.com') + + // Save drawer + await linkDrawer.locator('button').getByText('Save').first().click() + await expect(linkDrawer).toBeHidden() + await wait(1500) + + // The entire link should be selected now => press arrow right to move cursor to the end of the link node before we type + await page.keyboard.press('ArrowRight') + // Just keep typing - the cursor should not have moved to the beginning of the richtext field + await page.keyboard.type(' xxx') + + await saveDocAndAssert(page) + + // Check if the text is bold. It's a self-relationship, so no need to follow relationship + await expect(async () => { + const lexicalDoc: LexicalField = ( + await payload.find({ + collection: lexicalFieldsSlug, + depth: 0, + overrideAccess: true, + where: { + title: { + equals: lexicalDocData.title, + }, + }, + }) + ).docs[0] as never + + const lexicalField: SerializedEditorState = lexicalDoc.lexicalRootEditor + + const firstParagraph: SerializedParagraphNode = lexicalField.root + .children[0] as SerializedParagraphNode + const secondParagraph: SerializedParagraphNode = lexicalField.root + .children[1] as SerializedParagraphNode + + expect(firstParagraph.children).toHaveLength(1) + expect((firstParagraph.children[0] as SerializedTextNode).text).toBe('Some Text') + + expect(secondParagraph.children).toHaveLength(3) + expect((secondParagraph.children[0] as SerializedTextNode).text).toBe('Hello ') + expect((secondParagraph.children[1] as SerializedLinkNode).type).toBe('link') + expect((secondParagraph.children[1] as SerializedLinkNode).children).toHaveLength(1) + expect( + ((secondParagraph.children[1] as SerializedLinkNode).children[0] as SerializedTextNode) + .text, + ).toBe('there') + expect((secondParagraph.children[2] as SerializedTextNode).text).toBe(' xxx') + }).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + }) + + test('lexical cursor / selection should be preserved when swapping upload field and clicking within with its list drawer', async () => { + await navigateToLexicalFields() + const richTextField = page.locator('.rich-text-lexical').first() + await richTextField.scrollIntoViewIfNeeded() + await expect(richTextField).toBeVisible() + + const paragraph = richTextField.locator('.LexicalEditorTheme__paragraph').first() + await paragraph.scrollIntoViewIfNeeded() + await expect(paragraph).toBeVisible() + + /** + * Type some text + */ + await paragraph.click() + await page.keyboard.type('Some Text') + + await page.keyboard.press('Enter') + + await page.keyboard.press('/') + await page.keyboard.type('Upload') + + // Create Upload node + const slashMenuPopover = page.locator('#slash-menu .slash-menu-popup') + await expect(slashMenuPopover).toBeVisible() + + const uploadSelectButton = slashMenuPopover.locator('button').first() + await expect(uploadSelectButton).toBeVisible() + await expect(uploadSelectButton).toContainText('Upload') + await uploadSelectButton.click() + await expect(slashMenuPopover).toBeHidden() + + await wait(500) // wait for drawer form state to initialize (it's a flake) + const uploadListDrawer = page.locator('dialog[id^=list-drawer_1_]').first() // IDs starting with list-drawer_1_ (there's some other symbol after the underscore) + await expect(uploadListDrawer).toBeVisible() + await wait(500) + + await uploadListDrawer.locator('button').getByText('payload.png').first().click() + await expect(uploadListDrawer).toBeHidden() + + const newUploadNode = richTextField.locator('.lexical-upload').first() + await newUploadNode.scrollIntoViewIfNeeded() + await expect(newUploadNode).toBeVisible() + + await expect(newUploadNode.locator('.lexical-upload__bottomRow')).toContainText('payload.png') + + await page.keyboard.press('ArrowLeft') + // Select "there" by pressing shift + arrow left + for (let i = 0; i < 4; i++) { + await page.keyboard.press('Shift+ArrowLeft') + } + + await newUploadNode.locator('.lexical-upload__swap-drawer-toggler').first().click() + + const uploadSwapDrawer = page.locator('dialog[id^=list-drawer_1_]').first() + await expect(uploadSwapDrawer).toBeVisible() + await wait(500) + + // Click anywhere in the drawer to make sure the cursor position is preserved + await uploadSwapDrawer.locator('.drawer__content').first().click() + + // click button with text content "payload.jpg" + await uploadSwapDrawer.locator('button').getByText('payload.jpg').first().click() + + await expect(uploadSwapDrawer).toBeHidden() + await wait(500) + + // press ctrl+B to bold the text previously selected (assuming it is still selected now, which it should be) + await page.keyboard.press('Meta+B') + // In case this is mac or windows + await page.keyboard.press('Control+B') + + await wait(500) + + await saveDocAndAssert(page) + + // Check if the text is bold. It's a self-relationship, so no need to follow relationship + await expect(async () => { + const lexicalDoc: LexicalField = ( + await payload.find({ + collection: lexicalFieldsSlug, + depth: 0, + overrideAccess: true, + where: { + title: { + equals: lexicalDocData.title, + }, + }, + }) + ).docs[0] as never + + const lexicalField: SerializedEditorState = lexicalDoc.lexicalRootEditor + + const firstParagraph: SerializedParagraphNode = lexicalField.root + .children[0] as SerializedParagraphNode + const secondParagraph: SerializedParagraphNode = lexicalField.root + .children[1] as SerializedParagraphNode + const uploadNode: SerializedUploadNode = lexicalField.root.children[2] as SerializedUploadNode + + expect(firstParagraph.children).toHaveLength(2) + expect((firstParagraph.children[0] as SerializedTextNode).text).toBe('Some ') + expect((firstParagraph.children[0] as SerializedTextNode).format).toBe(0) + expect((firstParagraph.children[1] as SerializedTextNode).text).toBe('Text') + expect((firstParagraph.children[1] as SerializedTextNode).format).toBe(1) + + expect(secondParagraph.children).toHaveLength(0) + + expect(uploadNode.relationTo).toBe('uploads') + }).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + }) + describe('localization', () => { test.skip('ensure simple localized lexical field works', async () => { await navigateToLexicalFields(true, true)