Block Editor: bulk workflow fire drops rich blocks without the advisory the single fire returns
Nobody has claimed this yet.
- 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
-
Create a content type with a Block Editor field (
body). -
Create two contentlets whose
bodyholds 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>"}}' -
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, andmessagescarries 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. -
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": []. -
Read either contentlet back: both stored documents are
<p>plain</p>— thedotContentnode is gone in both cases. Only thePUTcaller 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 singlePUTreturns. - 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.
-
firemultipartand any other endpoint funnelling throughMapToContentletPopulatorare 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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