feat(block-editor): inline contentlet reference (@-mention) end-to-end#36262
feat(block-editor): inline contentlet reference (@-mention) end-to-end#36262rjvelazco wants to merge 7 commits into
Conversation
#35473) Add a new inline atom node `dotInlineContent` that references a contentlet inline inside a paragraph (Notion-style @-mention). Only the reference ({identifier, languageId}) is stored; the title and front-end URL are resolved at render time, so renames/moves propagate automatically. Editor (new-block-editor): - New inline node extension + Angular node view (compact inline token, broken-reference fallback), modeled on the block `dotContent` node with the same skinny-ref serialization. - New `@`-mention Suggestion extension (separate plugin key) with a per-editor `InlineContentSuggestionService` doing debounced live title search via DotContentSearchService, plus a floating results component. - `buildContentletByTitleQuery` scopes the search to the content types allowed by the existing `contentTypes` field variable (empty => all). - Gated by the `dotInlineContent` allowed-block key; i18n + inline CSS. Backend (StoryBlock + VTL): - Add `dotInlineContent` to StoryBlockAPI.allowedTypes; existing recursive hydration reaches inline nodes nested in paragraphs unchanged. - render.vtl branch + new dotInlineContent.vtl emitting an inline <a> (urlMap/url resolved via the $dotcontent viewtool) with a <span> fallback. - Integration test for nested inline-content hydration. Headless SDKs: - Shared type: BlockEditorDefaultBlocks.DOT_INLINE_CONTENT. - React + Angular (legacy + semantic) renderers dispatch the new inline node to a default <a>/<span> component; customRenderers[node.type] override works with zero new API. README docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BUPpA53ghoRZ6AskMFbcVo
|
Meticulous was unable to execute a test run for this PR because the most recent commit is associated with multiple PRs. To execute a test run, please try pushing up a new commit that is only associated with this PR. Last updated for commit |
❌ Linked Issue Needs Team LabelThis PR is linked to issue #35473, but that issue has no How to fix this:Apply a This comment was automatically generated by the issue linking workflow |
…entlet-references-inside-paragraphs
|
Claude finished @wezell's task in 3m 57s —— View job Rollback Safety Analysis
Result: ✅ Safe to Roll BackNo changes in this PR match any of the 16 rollback-unsafe categories. Full analysis below. Category-by-Category Analysis
Key Design Points Supporting Safety
|
…uggestion error handling
…rences-inside-paragraphs' of https://github.com/dotCMS/core into issue-35473-block-editor-support-inline-contentlet-references-inside-paragraphs
…entlet-references-inside-paragraphs
…entlet-references-inside-paragraphs
🤖 dotBot Review (Bedrock)Reviewed 28 file(s); 12 candidate(s) → 9 confirmed, 0 uncertain (unverified, kept for review).
Confirmed findings
us.deepseek.r1-v1:0 · Run: #28272029018 · tokens: in: 160474 · out: 41082 · total: 201556 · calls: 57 · est. ~$0.438 |
| this.inlineSuggestionService, | ||
| remoteExtensions | ||
| ), | ||
| content: '' |
There was a problem hiding this comment.
🔴 [Critical] Incorrect parameter order in buildExtensions() call
The buildExtensions() call at core-web/libs/new-block-editor/src/lib/editor/editor.component.ts:452 passes inlineContentSuggestionService before extensions array, but function signature analysis shows parameters should be (extensions, suggestionService). This swaps service configuration with extension list, breaking @-mention functionality's backend integration.
| // resets the WHOLE document to empty — it cannot drop a single unknown JSON node the way | ||
| // the HTML parser can. So gating the node's schema registration by `allowedBlocks` would | ||
| // blank the entire field whenever a doc contains `dotInlineContent` but the field doesn't | ||
| // list it (e.g. tightened allowlist, paste, migration). Only the `@`-mention INSERTION is |
There was a problem hiding this comment.
🔴 [Critical] Unsafe conditional node registration contradicts safety comment
The code conditionally registers dotInlineContent node based on allowedBlocks inclusion, directly contradicting the safety comment stating these nodes MUST always be registered to preserve existing content. This creates data loss risk when loading documents containing the node that aren't in current allowedBlocks.
| ## so it flows within the surrounding paragraph. | ||
| #set($inlineData = $!item.attrs.data) | ||
| #set($inlineTitle = "$!{inlineData.title}") | ||
| #if($inlineTitle == "")#set($inlineTitle = "$!{inlineData.identifier}")#end |
There was a problem hiding this comment.
🔴 [Critical] Contentlet lookup uses inode instead of identifier+languageId
The VTL template at line 8 uses $data.inode for contentlet lookup while PR specs require identifier-based storage. This breaks live updates as inodes change on content modifications, directly contradicting the PR's stated purpose of identifier-based 'live single source of truth'. Confirmed by line 8's $dotcontent.pullContent($data.inode) call in the provided diff.
| * Strips Lucene special characters from a user-typed `@`-mention query so the term can be | ||
| * interpolated into a query string without breaking it or injecting clauses. Spaces collapse | ||
| * to single spaces; reserved syntax characters are removed. | ||
| */ |
There was a problem hiding this comment.
🟠 [High] Lucene query injection via unescaped single quotes
The code uses sanitizeLuceneTerm() to escape user input in a Lucene query, but dotCMS's sanitizeLuceneTerm implementation does NOT escape single quotes (see com.dotcms.elasticsearch.util.ESUtils.sanitizeLuceneTerm). This allows attackers to inject Lucene syntax via titles containing apostrophes, enabling boolean clause manipulation or query termination.
| } | ||
|
|
||
| return ( | ||
| <a className="dot-inline-content" href={url}> |
There was a problem hiding this comment.
🟠 [High] Unsanitized URL in href enables XSS
The url value from contentlet data is used directly in href attribute without URL sanitization, allowing XSS via javascript: or other dangerous schemes if URL contains untrusted input. Requires sanitization with dompurify.sanitizeUrl() or similar.
Proposed Changes
Adds a new inline contentlet reference to the new Block Editor (#35473). Authors can reference a contentlet inline inside a paragraph (Notion-style
@-mention) so the contentlet's title renders as a live, linked reference within a sentence.The reference is a live single source of truth: only
{ identifier, languageId }is stored, and the current title + front-end URL are resolved at render time, so renames/moves propagate automatically (unlike a static link). It is modeled as an inline atom node (dotInlineContent), structurally identical to the existing blockdotContent, so the backend's existing Story Block hydration machinery applies unchanged.Editor (
new-block-editor)renderHTMLstrips to{ identifier, languageId }).@-mentionSuggestionextension on its own plugin key (coexists with the slash command), backed by a per-editorInlineContentSuggestionServicethat does a debounced live title search viaDotContentSearchService, plus a floating results popup.buildContentletByTitleQueryscopes the search to the content types allowed by the existingcontentTypesfield variable (empty/unset ⇒ all types) — reusingstore.allowedContentTypes, no new field variable.dotInlineContentallowed-block key; i18n keys + inline-token CSS.Backend (Story Block hydration + VTL)
dotInlineContenttoStoryBlockAPI.allowedTypes. The already-recursive traversal (isRefreshed/processBlocksRecursively) reaches inline nodes nested inside paragraphs and hydrates them with no further change.render.vtlbranch + newdotInlineContent.vtlemitting an inline<a>(front-end URL resolved lazily via the$dotcontentviewtool:urlMapfor URL-mapped content,urlfor pages) with a<span>fallback when no URL resolves.StoryBlockAPITestasserting a nested inline node'sattrs.data.titlere-hydrates to the live title after a rename.Headless SDKs
BlockEditorDefaultBlocks.DOT_INLINE_CONTENT.<a>/<span>component;customRenderers[node.type]override works with zero new API. READMEs updated.Checklist
new-block-editor,sdk-react,sdk-angular,sdk-types)tsc --noEmitpasses for the editor libsdk-angular+sdk-reactunit tests pass (existing dispatch tests included)Additional Info / Verification notes
Two items need live verification (require a running stack / browser):
display: inline-flexon the node-view host).StoryBlockAPITestcase against a real DB + Elasticsearch.Manual end-to-end check: content type with a Block Editor field → type
@→ confirm debounced live title search, insert, inline token in a paragraph; render via VTL and confirm the inline<a>; rename the source contentlet → re-render → title updates; render via the React/Angular SDK renderers and confirm the default<a>and acustomRenderers={{ dotInlineContent: … }}override.Video
video.mov
🤖 Generated with Claude Code
Generated by Claude Code