Skip to content

feat(block-editor): inline contentlet reference (@-mention) end-to-end#36262

Draft
rjvelazco wants to merge 5 commits into
mainfrom
issue-35473-block-editor-support-inline-contentlet-references-inside-paragraphs
Draft

feat(block-editor): inline contentlet reference (@-mention) end-to-end#36262
rjvelazco wants to merge 5 commits into
mainfrom
issue-35473-block-editor-support-inline-contentlet-references-inside-paragraphs

Conversation

@rjvelazco

@rjvelazco rjvelazco commented Jun 22, 2026

Copy link
Copy Markdown
Member

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 block dotContent, so the backend's existing Story Block hydration machinery applies unchanged.

Permanent node name: dotInlineContent becomes the JSON type customers store forever (see new-block-editor/CLAUDE.md — "TipTap Node Names Are Immutable"). It cannot change after release without a data migration.

Editor (new-block-editor)

  • New inline node extension + Angular node view (compact inline token; broken-reference fallback renders the last-known title as a non-link "missing" token), reusing the block node's skinny-ref serialization (renderHTML strips to { identifier, languageId }).
  • New @-mention Suggestion extension on its own plugin key (coexists with the slash command), backed by a per-editor InlineContentSuggestionService that does a debounced live title search via DotContentSearchService, plus a floating results popup.
  • buildContentletByTitleQuery scopes the search to the content types allowed by the existing contentTypes field variable (empty/unset ⇒ all types) — reusing store.allowedContentTypes, no new field variable.
  • Gated by the dotInlineContent allowed-block key; i18n keys + inline-token CSS.

Backend (Story Block hydration + VTL)

  • Added dotInlineContent to StoryBlockAPI.allowedTypes. The already-recursive traversal (isRefreshed / processBlocksRecursively) reaches inline nodes nested inside paragraphs and hydrates them with no further change.
  • render.vtl branch + new dotInlineContent.vtl emitting an inline <a> (front-end URL resolved lazily via the $dotcontent viewtool: urlMap for URL-mapped content, url for pages) with a <span> fallback when no URL resolves.
  • Integration test in StoryBlockAPITest asserting a nested inline node's attrs.data.title re-hydrates to the live title after a rename.

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. READMEs updated.

Checklist

  • Lint passes (new-block-editor, sdk-react, sdk-angular, sdk-types)
  • tsc --noEmit passes for the editor lib
  • sdk-angular + sdk-react unit tests pass (existing dispatch tests included)
  • Added integration test for nested inline-content hydration

Additional Info / Verification notes

Two items need live verification (require a running stack / browser):

  1. The ngx-tiptap inline node-view renders inline within the paragraph (CSS forces display: inline-flex on the node-view host).
  2. The new StoryBlockAPITest case 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 a customRenderers={{ dotInlineContent: … }} override.

Video

video.mov

🤖 Generated with Claude Code


Generated by Claude Code

#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
@alwaysmeticulous

Copy link
Copy Markdown

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 4cbf147. This comment will update as new commits are pushed.

@rjvelazco rjvelazco changed the title feat(block-editor): inline contentlet reference (@-mention) end-to-en… feat(block-editor): inline contentlet reference (@-mention) end-to-end Jun 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

❌ Linked Issue Needs Team Label

This PR is linked to issue #35473, but that issue has no Team : * label. Every linked issue must be owned by a team for tracking and triage.

How to fix this:

Apply a Team : * label to the linked issue (e.g., Team : Scout, Team : Platform, Team : Falcon, Team : Maintenance). Then push a new commit or edit the PR description to re-run this check.


This comment was automatically generated by the issue linking workflow

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🤖 Bedrock Review — deepseek.v3.2

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/extensions/editor-extensions.ts:86-94 — Disabling link and underline in StarterKit is correct, but the comment says "StarterKit v3 bundles Link + Underline. Disable both here". However, the diff earlier in dot-block-editor.component.ts already disables them in the getStarterKitConfig method (lines 556-560). This duplication is unnecessary and could cause confusion if one location is changed but not the other. It's better to keep the configuration in one place (likely the component) to avoid inconsistency.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/extensions/inline-content-suggestion.extension.ts:44 — The allowSpaces: false configuration in the Suggestion plugin means the @-mention trigger will stop at the first space. This matches typical mention behavior, but ensure this is the intended design (e.g., @John Doe would only capture John). If the requirement is to allow spaces in the query, this should be true. The current implementation seems deliberate, but worth noting.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/services/inline-content-suggestion.service.ts:95 — In the open method, this.isLoading.set(true) is called before this.query$.next(query). However, if query is empty, the debounced observable may emit immediately (or after debounce) and set isLoading back to false. This is fine, but ensure the loading state correctly reflects the async request. The current flow looks correct.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/services/inline-content-suggestion.service.ts:157 — In the update method, this.isLoading.set(true) is set unconditionally on every keystroke, even if the previous request is still in flight. This could cause the loading indicator to flicker if requests overlap. Consider checking if a request is already pending before setting loading to true again. However, the debounce likely prevents rapid overlaps, so this is a minor issue.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/components/inline-content-suggestion/inline-content-suggestion.component.ts:139-141 — The afterRenderEffect for scrolling the active option uses this.service.activeIndex() directly. However, activeIndex is a signal; accessing it inside afterRenderEffect without being a dependency might cause it to not re-run when activeIndex changes. Since afterRenderEffect runs after every change detection, it might still work, but it's better to include activeIndex as a dependency for correctness.

[🟡 Medium] core-web/libs/sdk/angular/src/lib/components/dotcms-block-editor-renderer/blocks/inline-content.component.ts:40-46 — The ngOnInit logs an error if $data() is falsy. However, $data() is a computed signal that depends on node. If node changes later (e.g., due to dynamic content), the error might not be re-evaluated. Consider moving the check to a template or using an effect to log changes. This is a minor issue since node is likely stable.

[🟠 High] dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPI.java:32 — The allowedTypes set is updated to include "dotInlineContent". This is a critical change because it affects which node types are recognized and processed by the StoryBlockAPI. Ensure there are no other places in the backend that rely on this set (e.g., validation, serialization) that also need updating. The change appears correct and necessary for the feature.

[🟡 Medium] dotCMS/src/main/webapp/WEB-INF/velocity/static/storyblock/dotInlineContent.vtl:1-18 — The Velocity template assumes $dotcontent.find($inlineInode) returns a contentlet. If $inlineInode is empty or invalid, $inlineRef could be null, leading to no URL. This is handled by the #if($inlineRef) check, but ensure that the fallback (plain span) is the desired behavior. Also, note that $inlineInode is derived from $inlineData.inode, which might be undefined; the template uses "$!{inlineData.inode}" to avoid null errors, which is correct.

[🟡 Medium] dotcms-integration/src/test/java/com/dotcms/contenttype/business/StoryBlockAPITest.java:383-461 — The new integration test test_refresh_references_for_nested_inline_content is added. It tests the refresh logic for inline content references. Ensure the test covers edge cases like deleted contentlets (where $inlineRef is null) and missing identifiers. The test looks comprehensive but should also verify that the inline node remains inline (not converted to a block). The test does this by checking the nested structure, which is good.

[🟡 Medium] core-web/libs/edit-content/src/lib/fields/dot-edit-content-block-editor/dot-edit-content-block-editor.component.html:15 — The template change from @if (isNewBlockEditorEnabled()) to @if (isNewBlockEditorEnabled() === true) and adding @else if (isNewBlockEditorEnabled() === false) is more explicit and avoids rendering nothing when the value is null/undefined. This is a defensive improvement, but ensure the isNewBlockEditorEnabled() method returns a boolean and not a truthy/falsy value that could cause mismatches. The change is safe.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/extensions/nodes/inline-content/inline-content.extension.ts:44-70 — The parseHTML method handles both the new INLINE_CONTENT_HTML_HOST_TAG and a legacy span[data-type="dot-inline-content"]. This ensures backward compatibility if existing HTML uses the span format. However, note that the renderHTML only outputs the INLINE_CONTENT_HTML_HOST_TAG. Ensure that round-trip parsing and rendering don't lose attributes. The implementation looks correct.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/extensions/nodes/inline-content/inline-content.extension.ts:63-67 — In renderHTML, the data attribute is serialized as a JSON string containing only identifier and languageId (skinny ref). This matches the storage optimization described. However, ensure that the backend hydration can reconstruct the full data object from just these fields (it does, as per the existing dotContent logic). This is consistent with the block contentlet node.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/extensions/nodes/inline-content/inline-content.component.ts:31-34 — The node view uses [attr.title]="hoverTitle()" for a native tooltip. This is acceptable, but note that native tooltips may not be accessible for keyboard-only users. Consider adding aria-label or aria-describedby for screen readers. However, the token is already marked as selectable and has a role, so this is a minor accessibility gap.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/components/inline-content-suggestion/inline-content-suggestion.component.ts:115-117 — The onScroll event listener is added with capture: true. This is fine, but ensure that the scroll handler is lightweight (scrollTick.update is cheap). However, note that scrollTick is a signal, and updating it triggers effect re-runs (the afterRenderEffect that recomputes position). This could cause performance issues if scrolling rapidly. The debounce is not present; consider throttling or using requestAnimationFrame. This is a potential performance concern.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/components/slash-menu/slash-menu-catalog.ts:49-87 — The sanitizeLuceneTerm function removes Lucene special characters. This is important to prevent query injection. However, note that the sanitized term is then used in a wildcard query (*${term}*). If the term contains spaces, the wildcard may still match incorrectly (e.g., "foo bar" becomes *foo bar*). This is likely acceptable, but ensure it doesn't break search expectations. The function also collapses multiple spaces, which is good.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/components/slash-menu/slash-menu-catalog.ts:74-87 — The buildContentletByTitleQuery function uses +contentType:(Blog OR News) syntax. Note that the space inside the parentheses is an implicit OR in Lucene, but the explicit OR is also added. This might cause a syntax error if the types contain spaces themselves (they shouldn't, as content type names are unlikely to have spaces). The split and trim should handle this. It's safe.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/services/inline-content-suggestion.service.ts:180-212 — The


Run: #27984337861 · tokens: in: 18612 · out: 2048 · total: 20660

@github-actions github-actions Bot added Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code Area : SDK PR changes SDK libraries labels Jun 22, 2026
@claude

claude Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @rjvelazco's task in 2m 0s —— View job


Rollback Safety Analysis

  • Read rollback-unsafe categories reference
  • Get full PR diff
  • Analyze diff against all unsafe categories
  • Post results and apply label

Result: ✅ Safe To Rollback

After analyzing the full diff against all rollback-unsafe categories from docs/core/ROLLBACK_UNSAFE_CATEGORIES.md, no unsafe categories were found.

Categories Checked

Category Finding
C-1 Structural Data Model Change ❌ No DB schema changes whatsoever
C-2 Elasticsearch Mapping Change ❌ No ES mapping code touched
C-3 Content JSON Model Version Bump ❌ No CURRENT_MODEL_VERSION change
C-4 DROP TABLE / DROP COLUMN ❌ No DDL drops
H-1 One-Way Data Migration ❌ No runonce tasks or data transformations
H-2 RENAME TABLE / RENAME COLUMN ❌ No renames
H-3 PK Restructuring ❌ No constraint changes
H-4 New Content Type Field Type ❌ No new Field implementation registered
H-5 Storage Provider Change ❌ No storage provider change
H-6 DROP PROCEDURE / FUNCTION ❌ None dropped
H-7 NOT NULL column without default ❌ No DB columns added
H-8 VTL Viewtool Contract Change ❌ No viewtool signatures changed (see note below)
M-1 Non-Broadening Column Type Change ❌ No column type changes
M-2 Push Publishing Bundle Format Change ❌ No bundle XML changes
M-3 REST/GraphQL API Contract Change ❌ No REST endpoint contract changes
M-4 OSGi Plugin API Breakage ❌ No OSGi interface changes

What this PR changes

  • StoryBlockAPI.java — Adds "dotInlineContent" to the allowedTypes constant. This is purely additive: N-1 simply won't process nodes of this type (they will be ignored by the N-1 traversal), but N-1 starts and runs normally.
  • render.vtl / dotInlineContent.vtl — New VTL file added and referenced from render.vtl with a new elseif branch. N-1 lacks the dotInlineContent.vtl file and the elseif branch, but since dotInlineContent nodes would never have been created by N-1 and the VTL branch is purely additive, N-1 renders the same content it always did.
  • Language.properties — Additive i18n keys only.
  • Frontend / SDK — Pure frontend additions (Angular component, React component, TypeScript types). No backend contract change.
  • Integration tests — Test-only additions.

H-8 VTL Note

The new dotInlineContent.vtl calls $dotcontent.find(), $inlineRef.urlMap, and $inlineRef.url on the $dotcontent viewtool. These are existing methods on the viewtool — this PR does not change the viewtool's signature, return types, or any accessor on its returned objects. H-8 does not apply.

The label AI: Safe To Rollback has been applied.

…rences-inside-paragraphs' of https://github.com/dotCMS/core into issue-35473-block-editor-support-inline-contentlet-references-inside-paragraphs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code Area : SDK PR changes SDK libraries

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Block Editor: support inline contentlet references inside paragraphs

2 participants