dotCMS / dotCMS/core

Block Editor: bulk workflow fire drops rich blocks without the advisory the single fire returns

Open
#37,526 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

dotCMS : Rest API OKR : Customer Support Team : Modernization Type : Defect
Dominant language
Java
Stars
970
Forks
486
Avg merge
3d 33m
Merged PRs (30d)
170

Description

Problem Statement

A Markdown/HTML write to a Block Editor field fully replaces the stored document. When the submitted value does not carry the rich blocks the stored document holds (embedded contentlets, video, YouTube, grids), those blocks are destroyed. That is by design — but the save path builds an advisory warning so nothing is lost silently (#36658, #36841).

That advisory only reaches the client on the single-contentlet path. MapToContentletPopulator.popStoryBlockConversionMessages is called from the single-fire helper (WorkflowResource.java:3133) and from ContentResource.saveDraft (ContentResource.java:260), but never from the multi-contentlet path (fireMultipleActionDefault, WorkflowResource.java:3664). A bulk fire therefore destroys the same blocks with an empty messages array.

Impact: any client using the bulk path — content migrations, integrations, the MCP server, scripted imports — loses embedded contentlets with no signal at all. These are exactly the clients most likely to submit HTML/Markdown rather than hand-authored ProseMirror JSON, and least likely to notice the loss until an editor opens the content. Same populator, same data loss, no warning.

Found while building an end-to-end REST validation of #36659 / PR #36841 (the fix itself is sound — 85/85 assertions pass; this is an adjacent gap the validation surfaced).

Steps to Reproduce

  1. Create a content type with a Block Editor field (body).

  2. Create two contentlets whose body holds an embedded contentlet:

    curl -u admin@dotcms.com:admin -H 'Content-Type: application/json' \
      -X PUT "$URL/api/v1/workflow/actions/default/fire/PUBLISH?indexPolicy=WAIT_FOR" \
      -d '{"contentlet":{"contentType":"myType","title":"a",
           "body":"<p>intro</p><dotcms-content identifier=\"<REF_ID>\" language-id=\"1\"></dotcms-content>"}}'
    
  3. Overwrite the first one with plain HTML through the single path (PUT):

    curl -u admin@dotcms.com:admin -H 'Content-Type: application/json' \
      -X PUT "$URL/api/v1/workflow/actions/default/fire/PUBLISH?indexPolicy=WAIT_FOR" \
      -d '{"contentlet":{"contentType":"myType","identifier":"<A>","body":"<p>plain</p>"}}'
    

    200, and messages carries the advisory:

    Story Block field [body]: 1 rich block(s) in the stored document are not present in the
    submitted HTML and were replaced (dotContent 6591cf3a…). Carry them over as dotcms-* fences
    to preserve them.
    
  4. Overwrite the second one identically through the multi path (POST, same URL):

    curl -u admin@dotcms.com:admin -H 'Content-Type: application/json' \
      -X POST "$URL/api/v1/workflow/actions/default/fire/PUBLISH?indexPolicy=WAIT_FOR" \
      -d '{"contentlet":{"contentType":"myType","identifier":"<B>","body":"<p>plain</p>"}}'
    

    200, "messages": [].

  5. Read either contentlet back: both stored documents are <p>plain</p> — the dotContent node is gone in both cases. Only the PUT caller was told.

Expected: the multi-contentlet path surfaces the same advisory (per contentlet, so the caller can tell which one lost blocks).
Actual: messages is always empty on that path; the warnings are built, never popped, and discarded with the transient key.

Acceptance Criteria

  • A bulk fire (POST /api/v1/workflow/actions/default/fire/{systemAction}) that replaces stored rich blocks returns the same advisory the single PUT returns.
  • The advisory identifies which contentlet lost blocks — a bulk response covers many contentlets, so a bare message string is not enough.
  • A bulk fire that loses nothing returns no advisory (no noise on the common path).
  • The transient __storyBlockConversionWarnings__ key never leaks into any response entity map on the bulk path (it does not today — keep it that way).
  • Status codes are unchanged: advisory only, never a rejection.
  • firemultipart and any other endpoint funnelling through MapToContentletPopulator are audited for the same gap, and either wired up or explicitly documented as not carrying advisories.
  • Covered by a test that fires the same losing write through both paths and asserts both report it.

dotCMS Version

main — reproduced on a build from 2026-09-10 (1.0.0-SNAPSHOT). Behavior dates from the advisory's introduction in #36658 / PR #36709; PR #36841 extended the vocabulary but not the message wiring.

Severity

Medium - Some functionality impacted

Links

  • Related: #36659, PR #36841 (dotcms-* vocabulary on HTML input)
  • Related: #36658, PR #36709 (Markdown fences + the advisory this issue is about)
  • Freshdesk: NA

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with fireMultipleActionDefault in WorkflowResource.java:3664 and compare it with the single-fire path at WorkflowResource.java:3133 and ContentResource.saveDraft at line 260. Trace MapToContentletPopulator.popStoryBlockConversionMessages and the bulk response assembly. Done means per-contentlet advisories appear only when rich blocks are lost, the transient key stays out of responses, status codes are unchanged, and both paths are covered by a test.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.