36002 workflow fire convert block editor markdown to prosemirror json on save server side#36253
Conversation
…minators Add two pure helpers to TiptapMarkdown that the save path needs to safely ingest Story Block values: - isTiptapDoc(String): cheap detector for an already-valid Tiptap/ProseMirror document (peeks the first non-whitespace char before parsing), so editor- authored JSON can be stored unchanged instead of re-parsed as Markdown. - isMarkdownRepresentable(String): true only when every block is Markdown- expressible, used to refuse a Markdown overwrite that would silently drop rich blocks (dotContent, dotVideo, grid, etc.). Marks are ignored on purpose (losing a mark loses styling, not content). Covered by TiptapMarkdownDocDetectionTest (13 cases incl. nested rich blocks, marks-only docs, malformed/empty/null input). Refs #36002
…t save path
Wire the converter into MapToContentletPopulator.fillFields, the shared seam
that the workflow fire endpoints and the content REST API all funnel through.
For a Story Block field whose incoming value is Markdown (begins with neither
'{' nor '<'), convert it to a ProseMirror JSON document and store that, so non-
interactive clients (AI agents, headless imports) no longer require a human to
open and re-save the contentlet.
Guards:
- Already-valid Tiptap JSON and (deferred) HTML are stored unchanged.
- A Markdown update is refused when the existing stored document contains rich
blocks Markdown cannot represent, rather than silently destroying them.
- A conversion failure never blocks the save: the raw value is stored and a
warning logged (graceful degradation, consistent with #35728).
The converter stays pure; conversion and guards live at the ingestion seam.
Covered by StoryBlockMarkdownPopulatorTest (convert + GraphQL read-back, JSON
passthrough, HTML passthrough, primitive replace, rich-overwrite reject);
registered in MainSuite1b.
Refs #36002
…wn conversion The fire endpoints' Block Editor note promised Markdown/HTML acceptance but admitted it only took effect after a human re-saved in the editor — documenting the exact bug #36002 fixes. Update the shared @operation note to state that Markdown is converted to ProseMirror JSON automatically on save (and already- valid JSON is stored unchanged), drop the "converted when opened in the editor" caveat, and use a Markdown example. Regenerate openapi.yaml (all 6 fire operations share the constant). Refs #36002
|
Claude finished @hassandotcms's task in 2m 7s —— View job Rollback Safety Analysis
Result: Unsafe to Rollback (🟡 MEDIUM)Category matched: M-3 — REST / GraphQL / Headless API Contract Change The save path for Story Block fields now auto-converts Markdown → ProseMirror JSON on write. Clients that submit Markdown and read the field back on N will receive Evidence in the diff:
Actions taken:
|
🤖 Bedrock Review —
|
#36002 normalizes a plain-text/Markdown Story Block value to a ProseMirror JSON document on save, so a webPageContent `body` sent as plain text now reads back as a structured doc (object), not the raw string. Update the two API tests that asserted the old raw-string round-trip to assert the normalized doc instead, keeping plain-text input so they still exercise the server-side conversion: - Karate CheckingJSONAttributes.feature: assert body.type == 'doc' and the paragraph text, instead of body == "<raw string>". - Postman JsScriptAPI: assert body.type == 'doc' and that the surviving text segments are present (inline <b> markup is dropped by the Markdown converter), across fireNew/fireEdit/firePublish via the JS workflows viewtool. Refs #36002
…ntent checks CI surfaced two more API tests that asserted a webPageContent `body` (a Story Block field) read back as the raw plain-text string. #36002 now normalizes plain-text to a ProseMirror doc on save, so the field comes back as structured JSON. Update only the content (webPageContent) assertions to read the text from the doc — body.content[0].content[0].text — leaving the template `body` assertions (template markup, not a Story Block field) untouched: - BringBack: 3 content version checks (create/edit/bring-back). - VersionableResource: 1 content working-version check. Determined the complete affected set by parsing every collection's body assertions against its request endpoint (content vs template), so template, GraphQL seeded/bundle, and response-body assertions are correctly excluded. Refs #36002
…lmarkdown-to-prosemirror-json-on-save-server-side
🤖 Bedrock Review —
|
|
Pull Request Unsafe to Rollback!!!
|
| return isMarkdownRepresentable(MAPPER.readTree(tiptapJson)); | ||
| } catch (final java.io.IOException e) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Should we add some logging here ? in case of a failure
…a Story Block field JsScriptAPI runs after Bundle_Resource imports a bundle that downgrades webPageContent.body to a WYSIWYG field, so its body reads back as the raw string (HTML), not a ProseMirror doc. VersionableResource runs in the default-split instance where webPageContent.body is likewise a plain string. In both cases the markdown->ProseMirror conversion correctly does not apply, so revert these two collections to their original raw-string assertions. BringBack (runs before the bundle downgrade) and the Karate CheckingJSONAttributes test (separate instance) still assert the normalized doc, since they hit a genuine Story Block field.
🤖 Bedrock Review —
|
…flow-fire-convert-block-editor-htmlmarkdown-to-prosemirror-json-on-save-server-side
…f rejecting it When a Story Block field already holds rich blocks that Markdown cannot represent (embedded contentlets, video, layout grids) and a Markdown value is sent on the save path, keep the existing document and log a warning rather than throwing an exception. This preserves the rich content (no silent data loss) without interrupting the save flow, matching the documented contract that Markdown is for plain content; modifying such a field still requires a full Tiptap/ProseMirror JSON document. Update the fire endpoints' Block Editor note (regenerated openapi.yaml) to document this, and adjust the integration test to assert the existing document is preserved. Addresses review feedback on PR #36253.
Review polish on the #36002 discriminators: - isTiptapDoc: peek the first AND last non-whitespace character before parsing. - isMarkdownRepresentable: short-circuit blank input, and log at debug when a value is not parseable JSON instead of swallowing it silently. Addresses review feedback on PR #36253.
🤖 dotBot Review (Bedrock)Reviewed 9 file(s); 7 candidate(s) → 4 confirmed, 0 uncertain (unverified, kept for review). Confirmed findings
us.deepseek.r1-v1:0 · Run: #28385451717 · tokens: in: 68232 · out: 17275 · total: 85507 · calls: 21 · est. ~$0.185 |
|
Pull Request Unsafe to Rollback!!!
|
| // content only and must not be used to modify a field that already holds such blocks. If that | ||
| // is attempted, keep the existing document untouched and log a warning — neither destroying | ||
| // the rich content nor failing the save. (Markdown -> rich merge is planned as a follow-up.) | ||
| final String existing = contentlet.getStringProperty(field.getVelocityVarName()); |
There was a problem hiding this comment.
🟠 [High] Incorrect logger usage in MapToContentletPopulator
Logger.warn is called with 'this' (instance) instead of a proper logger category, potentially causing silent logging failures due to null category. The correct pattern is Logger.warn(MapToContentletPopulator.class, ...) or using a static logger instance.
| @@ -96,7 +96,8 @@ | |||
| com.dotcms.content.elasticsearch.business.ES6UpgradeTest.class, | |||
There was a problem hiding this comment.
🟠 [High] Missing test class reference in test suite
The test class 'StoryBlockMarkdownPopulatorTest' is added to the test suite in MainSuite1b.java, but a grep search reveals no such class exists in the repository. This results in a runtime error when the test suite is executed, as the referenced test class cannot be found.
fabrizzio-dotCMS
left a comment
There was a problem hiding this comment.
I'm approving it but Im keeping my concerns about raising the IllegalStateException
What
Converts Story Block (Block Editor) field values supplied as Markdown to Tiptap/ProseMirror JSON server-side, on the shared content save path. Non-interactive clients (AI agents, headless imports) no longer need a human to open and re-save the contentlet for the field to read back as structured content.
Closes #36002 (Markdown scope; see Scope below).
How
TiptapMarkdown.isTiptapDoc/isMarkdownRepresentable— pure discriminators.MapToContentletPopulator.fillFields— the seam shared by the workflow fire endpoints and the content REST API. For a Story Block value:{(JSON) or<(HTML) → stored unchanged;TiptapMarkdown.toTiptap.dotContent,dotVideo, grid, …) instead of silently destroying them; a conversion failure never blocks the save (stores raw + logs).Scope
Behavior change
corrupted the field. Additive and rollback-safe (stored JSON is read natively by N-1).
Testing
TiptapMarkdownDocDetectionTest(13) + existingTiptapMarkdownTest/RoundTripContractTest.StoryBlockMarkdownPopulatorTest— convert + GraphQL read-back, JSONpassthrough, HTML passthrough, primitive replace, rich-overwrite reject.
MapToContentletPopulatorTest(20),StoryBlockValidationTest(28).