From dd9133659c35b6dfa4f3960210d5cee60bdc0a59 Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Mon, 8 Apr 2024 22:19:37 -0400 Subject: [PATCH 1/8] ci: rework caching, consolidates build (#5737) --- .github/workflows/main.yml | 66 ++++++++++---------------------------- 1 file changed, 17 insertions(+), 49 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4d2d3641ac..2e4297d0a1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,7 +39,7 @@ jobs: echo "needs_build: ${{ steps.filter.outputs.needs_build }}" echo "templates: ${{ steps.filter.outputs.templates }}" - core-build: + build: needs: changes if: ${{ needs.changes.outputs.needs_build == 'true' }} runs-on: ubuntu-latest @@ -65,65 +65,29 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v4 - name: Setup pnpm cache + - name: Setup pnpm cache + uses: actions/cache@v4 + timeout-minutes: 720 with: path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-pnpm-store- - ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + pnpm-store- + pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - run: pnpm install - - run: pnpm run build:core + - run: pnpm run build:all - name: Cache build uses: actions/cache@v4 + timeout-minutes: 10 with: path: ./* key: ${{ github.sha }}-${{ github.run_number }} - plugins-build: - needs: changes - if: ${{ needs.changes.outputs.needs_build == 'true' }} - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 25 - - - name: Use Node.js 18 - uses: actions/setup-node@v4 - with: - node-version: 18 - - - name: Install pnpm - uses: pnpm/action-setup@v3 - with: - version: 8 - run_install: false - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - uses: actions/cache@v4 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - - - run: pnpm install - - run: pnpm run build:plugins - tests-unit: runs-on: ubuntu-latest - needs: core-build + needs: build if: false # Disable until tests are updated for 3.0 steps: @@ -140,6 +104,7 @@ jobs: - name: Restore build uses: actions/cache@v4 + timeout-minutes: 10 with: path: ./* key: ${{ github.sha }}-${{ github.run_number }} @@ -151,7 +116,7 @@ jobs: tests-int: runs-on: ubuntu-latest - needs: core-build + needs: build strategy: fail-fast: false matrix: @@ -184,6 +149,7 @@ jobs: - name: Restore build uses: actions/cache@v4 + timeout-minutes: 10 with: path: ./* key: ${{ github.sha }}-${{ github.run_number }} @@ -242,7 +208,7 @@ jobs: tests-e2e: runs-on: ubuntu-latest - needs: core-build + needs: build strategy: fail-fast: false matrix: @@ -279,6 +245,7 @@ jobs: - name: Restore build uses: actions/cache@v4 + timeout-minutes: 10 with: path: ./* key: ${{ github.sha }}-${{ github.run_number }} @@ -299,7 +266,7 @@ jobs: tests-type-generation: if: false # This should be replaced with gen on a real Payload project runs-on: ubuntu-latest - needs: core-build + needs: build steps: - name: Use Node.js 18 @@ -315,6 +282,7 @@ jobs: - name: Restore build uses: actions/cache@v4 + timeout-minutes: 10 with: path: ./* key: ${{ github.sha }}-${{ github.run_number }} From 75cab7688fdf3362f5a33ee35c09941bb6f427cd Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Tue, 9 Apr 2024 09:26:09 -0400 Subject: [PATCH 2/8] chore: fix incorrect next tsconfig paths breaking monorepo setup (#5743) --- packages/next/src/views/NotFound/index.tsx | 2 +- tsconfig.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/src/views/NotFound/index.tsx b/packages/next/src/views/NotFound/index.tsx index d70de66b8e..bc3f57b819 100644 --- a/packages/next/src/views/NotFound/index.tsx +++ b/packages/next/src/views/NotFound/index.tsx @@ -2,12 +2,12 @@ import type { I18n } from '@payloadcms/translations' import type { Metadata } from 'next' import type { AdminViewComponent, SanitizedConfig } from 'payload/types' -import { getNextI18n } from '@payloadcms/next/utilities' import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser' import { DefaultTemplate } from '@payloadcms/ui/templates/Default' import React, { Fragment } from 'react' import { initPage } from '../../utilities/initPage.js' +import { getNextI18n } from '.././../utilities/getNextI18n.js' import { NotFoundClient } from './index.client.js' export const generatePageMetadata = async ({ diff --git a/tsconfig.json b/tsconfig.json index 40316db156..f4411fe8cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -82,7 +82,7 @@ "./packages/ui/src/scss/app.scss" ], "@payloadcms/next/*": [ - "./packages/next/src/exports/*" + "./packages/next/src/*" ], "@payloadcms/next": [ "./packages/next/src/exports/*" @@ -161,4 +161,4 @@ ".next/types/**/*.ts", "scripts/**/*.ts" ] -} \ No newline at end of file +} From 1a975b31cf75f42ced7472905211d2b3c0a81ea9 Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Tue, 9 Apr 2024 09:26:41 -0400 Subject: [PATCH 3/8] chore(release): v3.0.0-alpha.60 [skip ci] --- package.json | 2 +- packages/create-payload-app/package.json | 2 +- packages/db-mongodb/package.json | 2 +- packages/db-postgres/package.json | 2 +- packages/graphql/package.json | 2 +- packages/next/package.json | 2 +- packages/payload/package.json | 2 +- packages/plugin-cloud-storage/package.json | 2 +- packages/plugin-cloud/package.json | 2 +- packages/plugin-form-builder/package.json | 2 +- packages/plugin-nested-docs/package.json | 2 +- packages/plugin-redirects/package.json | 2 +- packages/plugin-search/package.json | 2 +- packages/plugin-seo/package.json | 2 +- packages/richtext-lexical/package.json | 2 +- packages/richtext-slate/package.json | 2 +- packages/translations/package.json | 2 +- packages/ui/package.json | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index ec204bc31c..2a578fd892 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "payload-monorepo", - "version": "3.0.0-alpha.59", + "version": "3.0.0-alpha.60", "private": true, "type": "module", "workspaces:": [ diff --git a/packages/create-payload-app/package.json b/packages/create-payload-app/package.json index a56cff25f7..0c2682b48b 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-alpha.59", + "version": "3.0.0-alpha.60", "license": "MIT", "type": "module", "homepage": "https://payloadcms.com", diff --git a/packages/db-mongodb/package.json b/packages/db-mongodb/package.json index 6de410c56d..9b68bbcc99 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-alpha.59", + "version": "3.0.0-alpha.60", "description": "The officially supported MongoDB database adapter for Payload", "repository": { "type": "git", diff --git a/packages/db-postgres/package.json b/packages/db-postgres/package.json index c72f29b6cb..4ec3be5cb4 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-alpha.59", + "version": "3.0.0-alpha.60", "description": "The officially supported Postgres database adapter for Payload", "repository": { "type": "git", diff --git a/packages/graphql/package.json b/packages/graphql/package.json index f7993cad45..ff2095c4cf 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/graphql", - "version": "3.0.0-alpha.59", + "version": "3.0.0-alpha.60", "main": "./src/index.ts", "types": "./src/index.d.ts", "type": "module", diff --git a/packages/next/package.json b/packages/next/package.json index 7f0f168d3b..c68d5ffff3 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/next", - "version": "3.0.0-alpha.59", + "version": "3.0.0-alpha.60", "main": "./src/index.js", "types": "./src/index.js", "type": "module", diff --git a/packages/payload/package.json b/packages/payload/package.json index 9d761f23f8..5c7de4bcea 100644 --- a/packages/payload/package.json +++ b/packages/payload/package.json @@ -1,6 +1,6 @@ { "name": "payload", - "version": "3.0.0-alpha.59", + "version": "3.0.0-alpha.60", "description": "Node, React and MongoDB Headless CMS and Application Framework", "license": "MIT", "main": "./src/index.ts", diff --git a/packages/plugin-cloud-storage/package.json b/packages/plugin-cloud-storage/package.json index 5be7e024a4..f405d0e375 100644 --- a/packages/plugin-cloud-storage/package.json +++ b/packages/plugin-cloud-storage/package.json @@ -1,7 +1,7 @@ { "name": "@payloadcms/plugin-cloud-storage", "description": "The official cloud storage plugin for Payload CMS", - "version": "3.0.0-alpha.59", + "version": "3.0.0-alpha.60", "main": "./src/index.ts", "types": "./src/index.ts", "type": "module", diff --git a/packages/plugin-cloud/package.json b/packages/plugin-cloud/package.json index e6c9eb2eca..3ba9f4ac63 100644 --- a/packages/plugin-cloud/package.json +++ b/packages/plugin-cloud/package.json @@ -1,7 +1,7 @@ { "name": "@payloadcms/plugin-cloud", "description": "The official Payload Cloud plugin", - "version": "3.0.0-alpha.59", + "version": "3.0.0-alpha.60", "main": "./src/index.ts", "types": "./src/index.ts", "license": "MIT", diff --git a/packages/plugin-form-builder/package.json b/packages/plugin-form-builder/package.json index a0971ea42c..1c883b6151 100644 --- a/packages/plugin-form-builder/package.json +++ b/packages/plugin-form-builder/package.json @@ -1,7 +1,7 @@ { "name": "@payloadcms/plugin-form-builder", "description": "Form builder plugin for Payload CMS", - "version": "3.0.0-alpha.59", + "version": "3.0.0-alpha.60", "homepage:": "https://payloadcms.com", "repository": { "type": "git", diff --git a/packages/plugin-nested-docs/package.json b/packages/plugin-nested-docs/package.json index 160835a94d..bbf42e969c 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-alpha.59", + "version": "3.0.0-alpha.60", "description": "The official Nested Docs plugin for Payload", "repository": { "type": "git", diff --git a/packages/plugin-redirects/package.json b/packages/plugin-redirects/package.json index 905931424a..cc228114a2 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-alpha.59", + "version": "3.0.0-alpha.60", "homepage:": "https://payloadcms.com", "repository": { "type": "git", diff --git a/packages/plugin-search/package.json b/packages/plugin-search/package.json index eaf1ed5295..fa4424e91c 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-alpha.59", + "version": "3.0.0-alpha.60", "homepage:": "https://payloadcms.com", "repository": { "type": "git", diff --git a/packages/plugin-seo/package.json b/packages/plugin-seo/package.json index cbadbca0a1..595c5b5909 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-alpha.59", + "version": "3.0.0-alpha.60", "homepage:": "https://payloadcms.com", "repository": { "type": "git", diff --git a/packages/richtext-lexical/package.json b/packages/richtext-lexical/package.json index 0294df53e1..4f0a9d9f30 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-alpha.59", + "version": "3.0.0-alpha.60", "description": "The officially supported Lexical richtext adapter for Payload", "repository": { "type": "git", diff --git a/packages/richtext-slate/package.json b/packages/richtext-slate/package.json index ad507b8ff0..7091415b9d 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-alpha.59", + "version": "3.0.0-alpha.60", "description": "The officially supported Slate richtext adapter for Payload", "repository": { "type": "git", diff --git a/packages/translations/package.json b/packages/translations/package.json index ec569d6fd8..4ecf430e3c 100644 --- a/packages/translations/package.json +++ b/packages/translations/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/translations", - "version": "3.0.0-alpha.59", + "version": "3.0.0-alpha.60", "main": "./src/exports/index.ts", "types": "./src/types.ts", "type": "module", diff --git a/packages/ui/package.json b/packages/ui/package.json index 2fa5575a44..32690f0b42 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@payloadcms/ui", - "version": "3.0.0-alpha.59", + "version": "3.0.0-alpha.60", "type": "module", "homepage": "https://payloadcms.com", "repository": { From 5826048e7becb8dc0aec510e03476c9f31442a59 Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Tue, 9 Apr 2024 09:50:15 -0400 Subject: [PATCH 4/8] ci: publish script throttling --- scripts/release.ts | 92 +++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/scripts/release.ts b/scripts/release.ts index dd270a6d84..6eeb1ac4eb 100755 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -6,19 +6,23 @@ import execa from 'execa' import fse from 'fs-extra' import minimist from 'minimist' import { fileURLToPath } from 'node:url' -import pMap from 'p-map' +import pLimit from 'p-limit' import path from 'path' import prompts from 'prompts' import semver from 'semver' import { simpleGit } from 'simple-git' +import type { PackageDetails } from './lib/getPackageDetails.js' + import { getPackageDetails } from './lib/getPackageDetails.js' import { updateChangelog } from './utils/updateChangelog.js' +const npmPublishLimit = pLimit(2) + // Update this list with any packages to publish const packageWhitelist = [ 'payload', - // 'translations', + 'translations', 'ui', 'next', 'graphql', @@ -27,7 +31,7 @@ const packageWhitelist = [ 'richtext-slate', 'richtext-lexical', - // 'create-payload-app', + 'create-payload-app', // Plugins 'plugin-cloud', @@ -143,26 +147,13 @@ async function main() { await execa('pnpm', ['install'], execaOpts) - const buildResult = await execa('pnpm', ['build:core', '--output-logs=errors-only'], execaOpts) - // const buildResult = execSync('pnpm build:all', execOpts) + const buildResult = await execa('pnpm', ['build:all', '--output-logs=errors-only'], execaOpts) if (buildResult.exitCode !== 0) { console.error(chalk.bold.red('Build failed')) console.log(buildResult.stderr) abort('Build failed') } - const buildPluginsResult = await execa( - 'pnpm', - ['build:plugins', '--output-logs=errors-only'], - execaOpts, - ) - - if (buildPluginsResult.exitCode !== 0) { - console.error(chalk.bold.red('Build failed')) - console.log(buildPluginsResult.stderr) - abort('Build failed') - } - // Update changelog if (changelog) { header(`${logPrefix}šŸ“ Updating changelog...`) @@ -209,35 +200,8 @@ async function main() { abort('2FA code is required') } - // Publish - const results: { name: string; success: boolean; details?: string }[] = await Promise.all( - packageDetails.map(async (pkg) => { - try { - console.log(logPrefix, chalk.bold(`šŸš€ ${pkg.name} publishing...`)) - const cmdArgs = ['publish', '-C', pkg.packagePath, '--no-git-checks', '--tag', tag] - if (dryRun) { - cmdArgs.push('--dry-run') - } else { - cmdArgs.push('--otp', otp) - } - const { exitCode, stderr } = await execa('pnpm', cmdArgs, { - cwd, - stdio: ['ignore', 'ignore', 'pipe'], - // stdio: 'inherit', - }) - - if (exitCode !== 0) { - console.log(chalk.bold.red(`\n\nāŒ ${pkg.name} ERROR: pnpm publish failed\n\n`)) - return { name: pkg.name, success: false, details: stderr } - } - - console.log(`${logPrefix} ${chalk.green(`āœ… ${pkg.name} published`)}`) - return { name: pkg.name, success: true } - } catch (error) { - console.error(chalk.bold.red(`\n\nāŒ ${pkg.name} ERROR: ${error.message}\n\n`)) - return { name: pkg.name, success: false } - } - }), + const results = await Promise.all( + packageDetails.map((pkg) => publishPackageThrottled(pkg, { dryRun, otp })), ) console.log(chalk.bold.green(`\n\nResults:\n`)) @@ -270,6 +234,42 @@ main().catch((error) => { process.exit(1) }) +/** Publish with promise concurrency throttling */ +async function publishPackageThrottled( + pkg: PackageDetails, + opts?: { dryRun?: boolean; otp?: string }, +) { + const { dryRun = false, otp } = opts ?? {} + return npmPublishLimit(() => publishSinglePackage(pkg, { dryRun, otp })) +} + +async function publishSinglePackage( + pkg: PackageDetails, + opts?: { dryRun?: boolean; otp?: string }, +) { + const { dryRun = false, otp } = opts ?? {} + console.log(chalk.bold(`šŸš€ ${pkg.name} publishing...`)) + const cmdArgs = ['publish', '-C', pkg.packagePath, '--no-git-checks', '--json', '--tag', tag] + if (dryRun) { + cmdArgs.push('--dry-run') + } else { + cmdArgs.push('--otp', otp) + } + const { exitCode, stderr } = await execa('pnpm', cmdArgs, { + cwd, + stdio: ['ignore', 'ignore', 'pipe'], + // stdio: 'inherit', + }) + + if (exitCode !== 0) { + console.log(chalk.bold.red(`\n\nāŒ ${pkg.name} ERROR: pnpm publish failed\n\n${stderr}`)) + return { name: pkg.name, success: false, details: stderr } + } + + console.log(`${logPrefix} ${chalk.green(`āœ… ${pkg.name} published`)}`) + return { name: pkg.name, success: true } +} + function abort(message = 'Abort', exitCode = 1) { console.error(chalk.bold.red(`\n${message}\n`)) process.exit(exitCode) From e73e610669591285d6bc68640e976b6142e4d02d Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Tue, 9 Apr 2024 12:22:42 -0400 Subject: [PATCH 5/8] chore: fields test suite: clearAndSeedEverything instead of seed for dev as well, to ensure same state as test runs (most importantly, this gets rid of leftover uploads) --- test/fields/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fields/config.ts b/test/fields/config.ts index 96e765092a..db0fa8f645 100644 --- a/test/fields/config.ts +++ b/test/fields/config.ts @@ -26,7 +26,7 @@ import Uploads from './collections/Upload/index.js' import Uploads2 from './collections/Upload2/index.js' import Uploads3 from './collections/Uploads3/index.js' import TabsWithRichText from './globals/TabsWithRichText.js' -import { seed } from './seed.js' +import { clearAndSeedEverything } from './seed.js' export const collectionSlugs: CollectionConfig[] = [ LexicalFields, @@ -79,7 +79,7 @@ export default buildConfigWithDefaults({ }, onInit: async (payload) => { if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') { - await seed(payload) + await clearAndSeedEverything(payload) } }, }) From 607ff1703382a328ff793305ed0b39080b6e6503 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Tue, 9 Apr 2024 12:24:30 -0400 Subject: [PATCH 6/8] fix(richtext-lexical): upload nodes weren't visible due to incorrect relationships condition --- .../relationship/utils/EnabledRelationshipsCondition.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/richtext-lexical/src/field/features/relationship/utils/EnabledRelationshipsCondition.tsx b/packages/richtext-lexical/src/field/features/relationship/utils/EnabledRelationshipsCondition.tsx index f3d1c72531..8bcf4639c2 100644 --- a/packages/richtext-lexical/src/field/features/relationship/utils/EnabledRelationshipsCondition.tsx +++ b/packages/richtext-lexical/src/field/features/relationship/utils/EnabledRelationshipsCondition.tsx @@ -19,7 +19,7 @@ type FilteredCollectionsT = ( const filterRichTextCollections: FilteredCollectionsT = (collections, options) => { return collections.filter(({ slug, admin: { enableRichTextRelationship }, upload }) => { - if (options.visibleEntities.collections.includes(slug)) { + if (!options.visibleEntities.collections.includes(slug)) { return false } From ec0e0ae449d985b8c026c9674e71878d2943934d Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Tue, 9 Apr 2024 12:24:54 -0400 Subject: [PATCH 7/8] chore(richtext-lexical): add e2e test to ensure that pre-seeded upload nodes are visible --- test/fields/lexical.e2e.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/fields/lexical.e2e.spec.ts b/test/fields/lexical.e2e.spec.ts index 0ea4e229f9..75b0a4f3ff 100644 --- a/test/fields/lexical.e2e.spec.ts +++ b/test/fields/lexical.e2e.spec.ts @@ -798,6 +798,22 @@ describe('lexical', () => { await shouldRespectRowRemovalTest() }) + test('ensure pre-seeded uploads node is visible', async () => { + // Due to issues with the relationships condition, we had issues with that not being visible. Checking for visibility ensures there is no breakage there again + await navigateToLexicalFields() + const richTextField = page.locator('.rich-text-lexical').nth(1) // second + await richTextField.scrollIntoViewIfNeeded() + await expect(richTextField).toBeVisible() + + const uploadBlock = richTextField.locator('.ContentEditable__root > div').first() // Check for the first div, as we wanna make sure it's the first div in the editor (1. node is a paragraph, second node is a div which is the upload node) + await uploadBlock.scrollIntoViewIfNeeded() + await expect(uploadBlock).toBeVisible() + + await expect(uploadBlock.locator('.lexical-upload__doc-drawer-toggler strong')).toHaveText( + 'payload.jpg', + ) + }) + test.skip('should respect required error state in deeply nested text field', async () => { await navigateToLexicalFields() const richTextField = page.locator('.rich-text-lexical').nth(1) // second From af40302e5ff9f9764d0bd537f3c4633e67268932 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Tue, 9 Apr 2024 12:29:45 -0400 Subject: [PATCH 8/8] fix(richtext-lexical): get links to work again (#5745) --- .../src/field/features/link/nodes/LinkNode.ts | 4 +- .../floatingLinkEditor/LinkEditor/index.tsx | 27 ++++-- .../link/plugins/floatingLinkEditor/types.ts | 3 + test/fields/lexical.e2e.spec.ts | 90 ++++++++++++++++++- 4 files changed, 114 insertions(+), 10 deletions(-) diff --git a/packages/richtext-lexical/src/field/features/link/nodes/LinkNode.ts b/packages/richtext-lexical/src/field/features/link/nodes/LinkNode.ts index 7ba74f1846..5846d8aa96 100644 --- a/packages/richtext-lexical/src/field/features/link/nodes/LinkNode.ts +++ b/packages/richtext-lexical/src/field/features/link/nodes/LinkNode.ts @@ -246,10 +246,10 @@ export const TOGGLE_LINK_COMMAND: LexicalCommand = export function toggleLink(payload: LinkPayload): void { const selection = $getSelection() - if (!$isRangeSelection(selection)) { + if (!$isRangeSelection(selection) && !payload.selectedNodes.length) { return } - const nodes = selection.extract() + const nodes = $isRangeSelection(selection) ? selection.extract() : payload.selectedNodes if (payload === null) { // Remove LinkNodes diff --git a/packages/richtext-lexical/src/field/features/link/plugins/floatingLinkEditor/LinkEditor/index.tsx b/packages/richtext-lexical/src/field/features/link/plugins/floatingLinkEditor/LinkEditor/index.tsx index 2ec17c9179..16179480c0 100644 --- a/packages/richtext-lexical/src/field/features/link/plugins/floatingLinkEditor/LinkEditor/index.tsx +++ b/packages/richtext-lexical/src/field/features/link/plugins/floatingLinkEditor/LinkEditor/index.tsx @@ -8,6 +8,8 @@ const { useLexicalComposerContext } = lexicalComposerContextImport import lexicalUtilsImport from '@lexical/utils' const { $findMatchingParent, mergeRegister } = lexicalUtilsImport +import type { LexicalNode } from 'lexical' + import { getTranslation } from '@payloadcms/translations' import lexicalImport from 'lexical' const { @@ -57,6 +59,8 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R const { closeModal, toggleModal } = useModal() const editDepth = useEditDepth() const [isLink, setIsLink] = useState(false) + const [selectedNodes, setSelectedNodes] = useState([]) + const [isAutoLink, setIsAutoLink] = useState(false) const drawerSlug = formatDrawerSlug({ @@ -78,6 +82,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R setIsAutoLink(false) setLinkUrl('') setLinkLabel('') + setSelectedNodes([]) return } @@ -115,6 +120,8 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R setStateData(data) setIsLink(true) + setSelectedNodes(selection ? selection?.getNodes() : []) + if ($isAutoLinkNode(linkParent)) { setIsAutoLink(true) } else { @@ -291,18 +298,26 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R const newLinkPayload: LinkPayload = data as LinkPayload + newLinkPayload.selectedNodes = selectedNodes + // See: https://github.com/facebook/lexical/pull/5536. This updates autolink nodes to link nodes whenever a change was made (which is good!). editor.update(() => { const selection = $getSelection() + let linkParent = null if ($isRangeSelection(selection)) { - const parent = getSelectedNode(selection).getParent() - if ($isAutoLinkNode(parent)) { - const linkNode = $createLinkNode({ - fields: newLinkPayload.fields, - }) - parent.replace(linkNode, true) + linkParent = getSelectedNode(selection).getParent() + } else { + if (selectedNodes.length) { + linkParent = selectedNodes[0].getParent() } } + + if (linkParent && $isAutoLinkNode(linkParent)) { + const linkNode = $createLinkNode({ + fields: newLinkPayload.fields, + }) + linkParent.replace(linkNode, true) + } }) // Needs to happen AFTER a potential auto link => link node conversion, as otherwise, the updated text to display may be lost due to diff --git a/packages/richtext-lexical/src/field/features/link/plugins/floatingLinkEditor/types.ts b/packages/richtext-lexical/src/field/features/link/plugins/floatingLinkEditor/types.ts index 2c9d2a3bee..30e103f97d 100644 --- a/packages/richtext-lexical/src/field/features/link/plugins/floatingLinkEditor/types.ts +++ b/packages/richtext-lexical/src/field/features/link/plugins/floatingLinkEditor/types.ts @@ -1,3 +1,5 @@ +import type { LexicalNode } from 'lexical' + import type { LinkFields } from '../../nodes/types.js' /** @@ -6,6 +8,7 @@ import type { LinkFields } from '../../nodes/types.js' */ export type LinkPayload = { fields: LinkFields + selectedNodes?: LexicalNode[] /** * The text content of the link node - will be displayed in the drawer */ diff --git a/test/fields/lexical.e2e.spec.ts b/test/fields/lexical.e2e.spec.ts index 0ea4e229f9..52a562054f 100644 --- a/test/fields/lexical.e2e.spec.ts +++ b/test/fields/lexical.e2e.spec.ts @@ -1,4 +1,4 @@ -import type { SerializedBlockNode } from '@payloadcms/richtext-lexical' +import type { SerializedBlockNode, SerializedLinkNode } from '@payloadcms/richtext-lexical' import type { Page } from '@playwright/test' import type { SerializedEditorState, SerializedParagraphNode, SerializedTextNode } from 'lexical' import type { Payload } from 'payload' @@ -401,7 +401,7 @@ describe('lexical', () => { for (let i = 0; i < 18; i++) { await page.keyboard.press('Shift+ArrowRight') } - // The following text should now be selected: elationship node 1 + // The following text should now be selectedelationship node 1 const floatingToolbar_formatSection = page.locator( '.floating-select-toolbar-popup__section-format', @@ -462,6 +462,92 @@ describe('lexical', () => { timeout: POLL_TOPASS_TIMEOUT, }) }) + + test('should be able to select text, make it an external link and receive the updated link value', async () => { + // Reproduces https://github.com/payloadcms/payload/issues/4025 + await navigateToLexicalFields() + const richTextField = page.locator('.rich-text-lexical').nth(1) // second + await richTextField.scrollIntoViewIfNeeded() + await expect(richTextField).toBeVisible() + + // Find span in contentEditable with text "Some text below relationship node" + const spanInEditor = richTextField.locator('span').getByText('Upload Node:').first() + await expect(spanInEditor).toBeVisible() + await spanInEditor.click() // Use click, because focus does not work + + await page.keyboard.press('ArrowRight') + // Now select some text + for (let i = 0; i < 4; i++) { + await page.keyboard.press('Shift+ArrowRight') + } + // The following text should now be "Node" + + const floatingToolbar = page.locator('.floating-select-toolbar-popup') + + await expect(floatingToolbar).toBeVisible() + + const linkButton = floatingToolbar + .locator('.floating-select-toolbar-popup__button-link') + .first() + + await expect(linkButton).toBeVisible() + await linkButton.click() + + /** + * In drawer + */ + const drawerContent = page.locator('.drawer__content').first() + await expect(drawerContent).toBeVisible() + + const urlField = drawerContent.locator('input#field-fields__url').first() + await expect(urlField).toBeVisible() + // Fill with https://www.payloadcms.com + await urlField.fill('https://www.payloadcms.com') + await expect(urlField).toHaveValue('https://www.payloadcms.com') + await drawerContent.locator('.form-submit button').click({ delay: 100 }) + await expect(drawerContent).toBeHidden() + + /** + * check if it worked correctly + */ + + const linkInEditor = richTextField.locator('a.LexicalEditorTheme__link').first() + await expect(linkInEditor).toBeVisible() + await expect(linkInEditor).toHaveAttribute('href', 'https://www.payloadcms.com') + + await saveDocAndAssert(page) + + // Check if it persists after saving + await expect(linkInEditor).toBeVisible() + await expect(linkInEditor).toHaveAttribute('href', 'https://www.payloadcms.com') + + // Make sure it's being returned from the API as well + await expect(async () => { + const lexicalDoc: LexicalField = ( + await payload.find({ + collection: lexicalFieldsSlug, + depth: 0, + where: { + title: { + equals: lexicalDocData.title, + }, + }, + }) + ).docs[0] as never + + const lexicalField: SerializedEditorState = lexicalDoc.lexicalWithBlocks + + expect( + ( + (lexicalField.root.children[0] as SerializedParagraphNode) + .children[1] as SerializedLinkNode + ).fields.url, + ).toBe('https://www.payloadcms.com') + }).toPass({ + timeout: POLL_TOPASS_TIMEOUT, + }) + }) + test('ensure slash menu is not hidden behind other blocks', async () => { // This test makes sure there are no z-index issues here await navigateToLexicalFields()