Skip to content

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

Draft
rjvelazco wants to merge 6 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 6 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 redundant with the same change already made in dot-block-editor.component.ts. This duplication could cause confusion if one location is updated but not the other.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/extensions/inline-content-suggestion.extension.ts:44 — The allowSpaces: false configuration may break the @-mention picker if a user types @some content. Consider if spaces should be allowed to support multi-word searches.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/extensions/inline-content-suggestion.extension.ts:84-90 — The onExit logic includes a guard to prevent premature closure, but relies on checking the plugin state. Ensure this guard works correctly during rapid typing or cursor movement to avoid leaving the picker open unintentionally.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/services/inline-content-suggestion.service.ts:57 — The catchError(() => of(null)) operator swallows all errors and returns null, making it impossible to distinguish between a network error and an empty result. This is fine for UI states (hasError), but consider logging the error for debugging.

[🟠 High] core-web/libs/new-block-editor/src/lib/editor/services/inline-content-suggestion.service.ts:83-86 — The catchError mapping to null means an empty result set (entity not null but contentlets empty) and a failed request both set hasError(true). This incorrectly marks a successful empty search as an error. Fix the logic: hasError should only be set when entity === null.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/services/inline-content-suggestion.service.ts:135 — The open method sets isLoading(true) synchronously, but the async search might not start if query$.next(query) is called after a zone run. This is likely fine due to the debounceTime, but ensure loading state is cleared if the query is debounced away.

[🟡 Medium] core-web/libs/new-block-editor/src/lib/editor/components/inline-content-suggestion/inline-content-suggestion.component.ts:119-121 — The afterRenderEffect for scrolling the active option uses this.service.activeIndex() directly. If activeIndex updates in the same tick as isOpen, the target element might not be rendered yet. This is probably safe due to Angular's change detection, but could cause a race.

[🟡 Medium] core-web/libs/sdk/angular/src/lib/components/dotcms-block-editor-renderer/blocks/inline-content.component.ts:42-45 — The ngOnInit error logging occurs even if $data() is initially null but becomes available later (e.g., async hydration). Consider moving the check to ngOnChanges or using an effect to log only after data is settled.

[🟡 Medium] dotCMS/src/main/java/com/dotcms/contenttype/business/StoryBlockAPI.java:32 — Adding "dotInlineContent" to allowedTypes is necessary for the backend hydration. Ensure there are no other hardcoded type checks elsewhere that need updating.

[🟡 Medium] dotCMS/src/main/webapp/WEB-INF/velocity/static/storyblock/dotInlineContent.vtl:15 — The VTL template uses $esc.html for the URL in the href attribute. While the title is escaped, the URL should also be escaped to prevent XSS if the URL contains malicious characters. Use $esc.html($inlineUrl) for the href value as well.


Run: #28037582108 · tokens: in: 18612 · out: 846 · total: 19458

@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 3m 20s —— View job


Rollback Safety Analysis — ✅ Safe To Rollback

  • Gather context from PR
  • Read rollback-unsafe categories reference
  • Get full PR diff
  • Analyze diff against unsafe categories
  • Apply appropriate label

Summary

All 28 changed files were reviewed against every unsafe category. No rollback-unsafe changes were found. Label AI: Safe To Rollback has been applied.

Category-by-Category Assessment

Category Finding
C-1 Structural Data Model Change ❌ No DB schema changes of any kind
C-2 ES Mapping Change ❌ No ES mapping code touched
C-3 Content JSON Model Version Bump CURRENT_MODEL_VERSION unchanged
C-4 DROP TABLE / DROP COLUMN ❌ No DDL drop statements
H-1 One-Way Data Migration ❌ No runonce task, no UPDATE-all backfill
H-2 RENAME TABLE / COLUMN ❌ None
H-3 PK Restructuring ❌ None
H-4 New Field Type ❌ No new Field subclass registered in FieldTypeAPI; dotInlineContent is a new TipTap/block-editor node type stored as JSON, not a dotCMS field type
H-5 Storage Provider Change ❌ None
H-6 DROP PROCEDURE / FUNCTION ❌ None
H-7 NOT NULL column without default ❌ No schema additions
H-8 VTL Viewtool Contract Change ❌ — see detailed note below
M-1 Non-broadening column type change ❌ None
M-2 Push Publishing Bundle Format ❌ No bundler changes
M-3 REST / GraphQL API Contract Change ❌ No endpoint signatures changed
M-4 OSGi Interface Change StoryBlockAPI interface gains only a new Set member value (additive), no method signatures changed

H-8 VTL Viewtool Contract — Detailed Note

The PR adds a new elseif branch in render.vtl dispatching dotInlineContent nodes to the new dotInlineContent.vtl, and a new VTL template that calls $dotcontent.find(...) — already-existing viewtool methods with unchanged signatures.

  • The $dotcontent.find() method is not new and its return type is unchanged.
  • The new VTL template (dotInlineContent.vtl) is a new file, so N-1 simply does not dispatch to it and no template is broken. If a dotInlineContent node appears in storage and N-1 renders the block, it silently falls through the elseif chain (no match ⇒ no output), which is graceful degradation.
  • No existing VTL template was modified in a way that breaks N-1 rendering.

This is purely additive and does not constitute a viewtool contract break under H-8.

SDK Types (BlockEditorDefaultBlocks)

Adding DOT_INLINE_CONTENT = 'dotInlineContent' to the enum is additive — SDK consumers that do not handle this value get a no-op / unknown-node fallback. No existing enum member was renamed or removed.


…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