Conversation
📝 WalkthroughWalkthroughThis PR adds a URL-preview system for the Lexical editor: new InsertTextPlugin (imperative insertion with smart spacing), StandaloneImageUrlPreviewPlugin (auto-convert standalone image URLs on space/paste), a URL preview transformer/constants, centralized GIF utilities, ImageNode/Component updates for preview rendering and loading fallback, and integration + tests across CreateDropInput/CreateDropContent. Changes
Sequence DiagramsequenceDiagram
actor User
participant Editor as Lexical Editor
participant StandalonePlugin as StandaloneImageUrlPreviewPlugin
participant InsertTextPlugin as InsertTextPlugin
participant ImageNode as Image Node
participant Transformer as UrlPreviewImageTransformer
User->>Editor: Type or paste an image URL
Editor->>StandalonePlugin: KEY_SPACE / PASTE event with token
StandalonePlugin->>StandalonePlugin: Parse token, validate image URL
StandalonePlugin->>ImageNode: Create image node (alt = URL_PREVIEW_IMAGE_ALT_TEXT)
StandalonePlugin->>Editor: Replace token with image node, adjust cursor
User->>Editor: Trigger InsertTextPlugin via ref (insertImagePreviewFromUrl)
InsertTextPlugin->>Editor: Insert image node with smart spacing
User->>Editor: Request markdown export
Editor->>Transformer: Inspect nodes
Transformer->>ImageNode: If alt == URL_PREVIEW_IMAGE_ALT_TEXT -> return src
Transformer-->>Editor: Export URL in markdown
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~35 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
components/drops/create/lexical/plugins/InsertTextPlugin.tsx (1)
156-156: Consider usingnullreturn instead of empty fragment.Returning
nullis slightly more idiomatic for components that render nothing, as it avoids creating an empty React fragment node.💡 Minor suggestion
- return <></>; + return null;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/drops/create/lexical/plugins/InsertTextPlugin.tsx` at line 156, In the InsertTextPlugin React component (InsertTextPlugin), replace the empty fragment return with returning null to avoid creating an unnecessary React node; locate the return statement that currently yields <> </> and change it so the component returns null when it should render nothing.components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.tsx (1)
53-65: Consider using a more accurate type annotation.The
textNodeparameter typeReturnType<typeof $createTextNode>is slightly misleading since this function receives an existing text node from the editor, not a newly created one. While functionally correct (both areTextNode), a more explicit type would improve clarity.💡 Suggested type improvement
+import type { TextNode } from "lexical"; + const replaceTokenWithPreviewNode = ({ textNode, start, end, url, appendTrailingSpace, }: { - readonly textNode: ReturnType<typeof $createTextNode>; + readonly textNode: TextNode; readonly start: number; readonly end: number; readonly url: string; readonly appendTrailingSpace: boolean; }): void => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.tsx` around lines 53 - 65, The parameter type for textNode in replaceTokenWithPreviewNode is misleadingly declared as ReturnType<typeof $createTextNode>; change it to the explicit TextNode type (imported as a type from 'lexical') and update the function signature accordingly, replacing ReturnType<typeof $createTextNode> with TextNode and adding the appropriate type-only import to the file so the annotation clearly reflects that the function receives an existing editor TextNode.__tests__/components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.test.tsx (1)
62-156: Good test coverage, consider adding Shift+Enter test.The tests cover space key and paste interactions well. Consider adding a test for the Shift+Enter behavior to ensure complete coverage of the plugin's command handlers.
💡 Suggested additional test case
it("converts standalone image URL token on shift+enter without preventing default", () => { const commandHandlers = new Map<string, (event?: any) => boolean>(); const editor = { registerCommand: jest.fn( (command: string, handler: (event?: any) => boolean) => { commandHandlers.set(command, handler); return () => {}; } ), update: (fn: () => void) => fn(), }; useLexicalComposerContextMock.mockReturnValue([editor]); const urlTextNode = { __isTextNode: true as const, getTextContent: () => "https://example.com/cat.gif", replace: jest.fn(), }; getSelectionMock.mockReturnValue({ isCollapsed: () => true, anchor: { getNode: () => urlTextNode, offset: "https://example.com/cat.gif".length, }, }); render(<StandaloneImageUrlPreviewPlugin />); const handled = commandHandlers.get("KEY_ENTER_COMMAND")?.({ shiftKey: true, preventDefault: jest.fn(), }); // Returns false to allow normal enter behavior expect(handled).toBe(false); expect(urlTextNode.replace).toHaveBeenCalled(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@__tests__/components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.test.tsx` around lines 62 - 156, Add a test for Shift+Enter behavior in StandaloneImageUrlPreviewPlugin: register the KEY_ENTER_COMMAND handler into commandHandlers (like other tests), mock a collapsed selection returning a urlTextNode (with getTextContent and replace), render StandaloneImageUrlPreviewPlugin, invoke the KEY_ENTER_COMMAND handler with an event containing shiftKey: true and a preventDefault mock, and assert the handler returns false (so default enter is allowed) and that urlTextNode.replace was still called; ensure you reference the same helpers/mocks used elsewhere (useLexicalComposerContextMock, getSelectionMock, commandHandlers map, urlTextNode.replace).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/drops/create/lexical/nodes/ImageComponent.tsx`:
- Around line 26-27: The code currently passes the sentinel
URL_PREVIEW_IMAGE_ALT_TEXT through to the DOM via alt (and variable
isUrlPreviewGif relies on it); update ImageComponent so the internal sentinel
value (URL_PREVIEW_IMAGE_ALT_TEXT) is never rendered to the alt attribute—map
that sentinel to a human-friendly alt or an empty string (e.g., alt="" for
decorative previews) before assigning to the DOM, while keeping isUrlPreviewGif,
altText and src logic intact; ensure any special handling for GIF previews still
uses the mapped/display alt and not the sentinel value.
In `@components/drops/create/lexical/transformers/UrlPreviewImageTransformer.ts`:
- Around line 19-20: The regExp /(?:)/ in UrlPreviewImageTransformer is a
zero-length pattern that matches anywhere and the replace: () => {} is a
dangerous no-op; change regExp to a non-matching pattern (e.g. /$^/) and make
replace a safe identity function (e.g. replace: (match) => match) so the
transformer’s regExp and replace fields are defensive and won’t accidentally
alter input if the transformer is later used in import flows.
---
Nitpick comments:
In
`@__tests__/components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.test.tsx`:
- Around line 62-156: Add a test for Shift+Enter behavior in
StandaloneImageUrlPreviewPlugin: register the KEY_ENTER_COMMAND handler into
commandHandlers (like other tests), mock a collapsed selection returning a
urlTextNode (with getTextContent and replace), render
StandaloneImageUrlPreviewPlugin, invoke the KEY_ENTER_COMMAND handler with an
event containing shiftKey: true and a preventDefault mock, and assert the
handler returns false (so default enter is allowed) and that urlTextNode.replace
was still called; ensure you reference the same helpers/mocks used elsewhere
(useLexicalComposerContextMock, getSelectionMock, commandHandlers map,
urlTextNode.replace).
In `@components/drops/create/lexical/plugins/InsertTextPlugin.tsx`:
- Line 156: In the InsertTextPlugin React component (InsertTextPlugin), replace
the empty fragment return with returning null to avoid creating an unnecessary
React node; locate the return statement that currently yields <> </> and change
it so the component returns null when it should render nothing.
In `@components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.tsx`:
- Around line 53-65: The parameter type for textNode in
replaceTokenWithPreviewNode is misleadingly declared as ReturnType<typeof
$createTextNode>; change it to the explicit TextNode type (imported as a type
from 'lexical') and update the function signature accordingly, replacing
ReturnType<typeof $createTextNode> with TextNode and adding the appropriate
type-only import to the file so the annotation clearly reflects that the
function receives an existing editor TextNode.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
__tests__/components/drops/create/lexical/nodes/ImageComponent.test.tsx__tests__/components/drops/create/lexical/plugins/InsertTextPlugin.test.tsx__tests__/components/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.test.tsx__tests__/components/drops/create/lexical/transformers/UrlPreviewImageTransformer.test.ts__tests__/components/waves/CreateDropContent.gifInsert.test.tsx__tests__/components/waves/CreateDropInput.test.tsxcomponents/drops/create/lexical/nodes/ImageComponent.tsxcomponents/drops/create/lexical/nodes/urlPreviewImage.constants.tscomponents/drops/create/lexical/plugins/InsertTextPlugin.tsxcomponents/drops/create/lexical/plugins/StandaloneImageUrlPreviewPlugin.tsxcomponents/drops/create/lexical/transformers/UrlPreviewImageTransformer.tscomponents/drops/view/part/dropPartMarkdown/handlers/gif.tsxcomponents/drops/view/part/dropPartMarkdown/renderers.tsxcomponents/waves/CreateDropContent.tsxcomponents/waves/CreateDropInput.tsxcomponents/waves/drops/gifPreview.ts
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/drops/create/lexical/plugins/InsertTextPlugin.tsx`:
- Around line 14-15: The plugin currently uses the generic sentinel string
"loading" which can collide with real image src values; define a shared,
namespaced constant (for example __URL_PREVIEW_LOADING__) in a central module
and replace all hard-coded "loading" usages in InsertTextPlugin
(InsertTextPlugin.tsx), the nodes ImageNode (ImageNode.tsx) and ImageComponent
(ImageComponent.tsx), and any other occurrences (the other referenced locations)
to import and use this constant instead so they all reference the same
non-colliding sentinel.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
__tests__/components/drops/create/lexical/nodes/ImageComponent.test.tsx__tests__/components/drops/create/lexical/nodes/ImageNode.test.tsx__tests__/components/drops/create/lexical/plugins/InsertTextPlugin.test.tsx__tests__/components/drops/create/lexical/transformers/UrlPreviewImageTransformer.test.tscomponents/drops/create/lexical/nodes/ImageComponent.tsxcomponents/drops/create/lexical/nodes/ImageNode.tsxcomponents/drops/create/lexical/plugins/InsertTextPlugin.tsxcomponents/drops/create/lexical/transformers/UrlPreviewImageTransformer.tscomponents/waves/CreateDropContent.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- tests/components/drops/create/lexical/transformers/UrlPreviewImageTransformer.test.ts
- components/waves/CreateDropContent.tsx
- components/drops/create/lexical/transformers/UrlPreviewImageTransformer.ts
| import { URL_PREVIEW_IMAGE_ALT_TEXT } from "../nodes/urlPreviewImage.constants"; | ||
| import { isTenorGifUrl } from "@/components/waves/drops/gifPreview"; |
There was a problem hiding this comment.
Use a non-colliding shared loading sentinel.
"loading" is too generic for a sentinel and can collide with legitimate src values. Please move to a shared, namespaced constant (e.g. __URL_PREVIEW_LOADING__) and reuse it across plugin/node/component paths.
🔧 Proposed fix
-import { URL_PREVIEW_IMAGE_ALT_TEXT } from "../nodes/urlPreviewImage.constants";
+import {
+ URL_PREVIEW_IMAGE_ALT_TEXT,
+ URL_PREVIEW_LOADING_SRC,
+} from "../nodes/urlPreviewImage.constants";
@@
-const LOADING_IMAGE_SRC = "loading";
@@
- src: shouldUseLoadingPlaceholder ? LOADING_IMAGE_SRC : normalizedUrl,
+ src: shouldUseLoadingPlaceholder
+ ? URL_PREVIEW_LOADING_SRC
+ : normalizedUrl,
@@
- if (placeholderNode.getSrc() !== LOADING_IMAGE_SRC) {
+ if (placeholderNode.getSrc() !== URL_PREVIEW_LOADING_SRC) {
return;
}Also update components/drops/create/lexical/nodes/ImageNode.tsx and components/drops/create/lexical/nodes/ImageComponent.tsx to consume the same shared constant.
Also applies to: 28-29, 138-138, 170-170
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/drops/create/lexical/plugins/InsertTextPlugin.tsx` around lines 14
- 15, The plugin currently uses the generic sentinel string "loading" which can
collide with real image src values; define a shared, namespaced constant (for
example __URL_PREVIEW_LOADING__) in a central module and replace all hard-coded
"loading" usages in InsertTextPlugin (InsertTextPlugin.tsx), the nodes ImageNode
(ImageNode.tsx) and ImageComponent (ImageComponent.tsx), and any other
occurrences (the other referenced locations) to import and use this constant
instead so they all reference the same non-colliding sentinel.



Summary by CodeRabbit
New Features
Improvements
Tests