Skip to content

Fix: Block editor charCount/wordCount/readingTime missing from Page API response#34952

Merged
zJaaal merged 5 commits intomainfrom
copilot/add-read-time-to-api
Mar 12, 2026
Merged

Fix: Block editor charCount/wordCount/readingTime missing from Page API response#34952
zJaaal merged 5 commits intomainfrom
copilot/add-read-time-to-api

Conversation

Copy link
Contributor

Copilot AI commented Mar 11, 2026

charCount, wordCount, and readingTime are visible in the Block Editor UI footer but were absent from the serialized JSON sent to the API. Root cause: onBlockEditorChange emitted the value immediately via onChange, while updateCharCount() (which dispatches the SetDocAttrStep attrs into the doc) runs 250 ms later behind a debounced (keyup). Any save within that window — or any programmatic write via writeValue — produced JSON without these attrs.

Changes

dot-block-editor.component.ts

  • In onBlockEditorChange, eagerly read live counts from TipTap's CharacterCountStorage and patch them into the doc attrs before calling onChange. No new transactions dispatched; safe against re-entrancy.
  • Added explicit void return type.
// characterCount is derived from editor?.storage — undefined before async ngOnInit completes
const charCount = this.characterCount?.characters?.() ?? 0;
const updatedValue: JSONContent =
    charCount > 0
        ? { ...value, attrs: { ...(value.attrs || {}), charCount,
              wordCount: this.characterCount?.words?.() ?? 0,
              readingTime: this.readingTime } }
        : value;
this.onChange?.(JSON.stringify(updatedValue));

dot-block-editor.component.spec.ts

  • Fixed confirmed false positive: blockEditorComponent.value = ... had no effect on the form; replaced with control.setValue(null) + onBlockEditorChange(...) to actually exercise the CVA onChange path.
  • Replaced all direct blockEditorComponent.disabled = true/false assignments with setDisabledState() (the CVA contract).
  • Added spectator.detectChanges() after setDisabledState() calls in the CSS-class template test.
  • Fixed typo: ControlValueAccesorControlValueAccessor.

Checklist

  • Tests
  • Translations
  • Security Implications Contemplated (add notes if applicable)

Additional Info

The debounced updateCharCount() path (keeping the internal ProseMirror doc attrs in sync for undo history) is untouched. The patch in onBlockEditorChange is purely for the outbound serialized value.

Screenshots

Original Updated
charCount/wordCount/readingTime absent from API JSON attrs present on every save — matches what the UI footer displays (2160 chars • 375 words • 2m read)
Original prompt

This section details on the original issue you should resolve

<issue_title>Read time visible in Block Editor but not available in Page API</issue_title>
<issue_description>### Problem Statement

I noticed that the read time, word count fields are available in the Block Editor UI. However, it does not seem to be included in the API response body.

Image

Steps to Reproduce

  1. Go to a Blog content entry that has the "Block Editor" field
  2. Open up the API response for that entry
  3. Search for read time in the json response, it will not be there

Acceptance Criteria

  • Read time and other data related to the block editor field is exposed in the API

dotCMS Version

latest

Severity

Low - Minor issue or cosmetic

Links

NA</issue_description>

<agent_instructions>I now have a complete picture. Here's my analysis of the bug:


Root Cause: Race Condition Between ngModelChange and updateCharCount

The bug is a timing issue in the frontend. Here's the sequence problem:

The Event Flow

In the template (dot-block-editor.component.html:13-14):

(keyup)="subject.next()"
(ngModelChange)="onBlockEditorChange($event)"

And the subscriptions (component.ts:364-366):

this.subject
    .pipe(takeUntil(this.destroy$), debounceTime(250))
    .subscribe(() => this.updateCharCount());

The Race

  1. User types → (ngModelChange) fires immediately, calling onBlockEditorChange()onChange(JSON.stringify(value))the value is sent to the form WITHOUT attrs (or with stale attrs)
  2. (keyup) fires → subject.next()250ms debounce → only THEN does updateCharCount() dispatch the SetDocAttrStep to inject charCount/wordCount/readingTime into the doc

The form control receives the JSON before updateCharCount() has run. If the user saves quickly (within 250ms of last keystroke), the submitted JSON won't have the updated attrs.

Secondary Issue: writeValue doesn't trigger updateCharCount

writeValue() (line 206-209) calls setEditorContent() but never calls updateCharCount(). The 'create' event handler does call it, but that only fires once on editor initialization — not when the form writes a new value programmatically.

The Specific Scenarios

Scenario Result
User types and saves quickly (<250ms) onChange fires with stale/missing attrs
Content loaded programmatically via API (no user interaction) attrs never injected via updateCharCount since no keyup fires
Editor opens with existing content 'create' fires updateCharCount() correctly
User pastes content ngModelChange fires immediately, keyup debounces 250ms

The Fix Area

onBlockEditorChange() at line 269 should also update the attrs before calling onChange, or at minimum the saved JSON should be taken after updateCharCount() runs. Currently:

onBlockEditorChange(value: JSONContent) {
    // value here may NOT have updated attrs yet!
    this.onChange?.(JSON.stringify(value));  // ← sent to form immediately
    // attrs only updated 250ms later via debounced subject
}

The fix would be to call updateCharCount() synchronously inside onBlockEditorChange before emitting, OR to serialize the editor state (which has the updated attrs) instead of the value argument.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@fmontes It works.

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

…onse

In onBlockEditorChange, eagerly patch the doc attrs from the current
CharacterCountStorage before emitting the value to the form control.
Previously, these attrs were only injected via updateCharCount() which
runs 250ms after the keyup event, causing the saved JSON to miss them.

Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
Copilot AI changed the title [WIP] Add read time and word count to Page API response Fix: Block editor charCount/wordCount/readingTime missing from API response Mar 11, 2026
Copy link
Contributor

@zJaaal zJaaal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: #34952 - Fix: Block editor charCount/wordCount/readingTime missing from API response

Summary

This PR fixes a bug where charCount, wordCount, and readingTime were not included in the Block Editor API response. The fix eagerly merges these stats into the JSONContent value before the debounced emit fires. The production change is small (19 lines), clean, and well-typed. However, the accompanying test suite has structural issues that undermine its reliability.

Files Changed: 2 frontend files
Risk Level: Low

Risk Assessment

Area Level Notes
Security None No security surface changes
Breaking Changes None Additive — new fields on existing object
Performance Impact None Synchronous property reads before existing emit
Test Coverage Partial Tests exist but have false positive and bypass issues

Frontend Findings

TypeScript Type Safety

Quality Issues 🔵 (75-84)

Missing explicit void return type on onBlockEditorChange (Confidence: 75)

// Current
onBlockEditorChange(value: JSONContent) {

// Fix
onBlockEditorChange(value: JSONContent): void {

Minor style alignment with the project's standards for explicit return types at public boundaries.


Angular Patterns

No issues found. The changed lines are purely logic within an existing method — no new Angular patterns, subscriptions, lifecycle hooks, or structural directives introduced.


Test Quality

Critical Issues 🔴 (95-100)

1. Direct property mutation bypasses Angular binding / ControlValueAccessor path (Confidence: 97)

Multiple tests directly set blockEditorComponent.value, blockEditorComponent.disabled, blockEditorComponent.editor, and blockEditorComponent.charLimit bypassing Angular's input binding pipeline and the ControlValueAccessor API.

// WRONG — bypasses CVA
blockEditorComponent.value = BLOCK_EDITOR_FIELD;

// CORRECT — exercises the CVA path
blockEditorComponent.writeValue(BLOCK_EDITOR_FIELD);

2. Line 66-73 test is a false positive (Confidence: 85)

The test "should set form value when binary file changes" passes trivially because the FormControl was already set to JSON.stringify(BLOCK_EDITOR_FIELD) during MockFormComponent construction. The blockEditorComponent.value = ... assignment does not invoke onChange and has no effect on the form. It never actually tests what the name claims.

// Fix: exercise the actual change path
blockEditorComponent.onBlockEditorChange(BLOCK_EDITOR_FIELD);
expect(spectator.component.form.get('block').value)
    .toEqual(JSON.stringify(BLOCK_EDITOR_FIELD));

Important Issues 🟡 (85-94)

3. Missing detectChanges after state mutations (Confidence: 96)
Tests mutate disabled and then assert without calling spectator.detectChanges(), meaning template-level effects are invisible.

4. No detectChanges: false in factory (Confidence: 88)
ngOnInit fires immediately on createComponent() with async service calls. Without controlled initialization, component may silently partially initialize if mocked providers don't cover all consumed services.

5. charLimit tested via direct mutation, not via fieldVariables lifecycle (Confidence: 86)
blockEditorComponent.charLimit = 100 bypasses the setFieldVariable() path called in ngOnInit. Tests can pass even if that lifecycle code is broken.

Quality Issues 🔵 (75-84)

6. Zero DOM/template assertions (Confidence: 78) — all tests assert on component properties only, not user-visible outcomes.

7. Typo in describe block name (Confidence: 76) — "ControlValueAccesor" should be "ControlValueAccessor".


Approval Recommendation

⚠️ Approve with Comments

The production fix is correct, small, and well-typed — the bug is genuinely addressed. The test suite, however, contains a confirmed false positive (the "should set form value" test always passes regardless of the production code) and multiple tests that bypass the lifecycle paths they claim to exercise.

Statistics:

  • Critical Issues: 2 (tests only)
  • Important Issues: 3 (tests only)
  • Quality Issues: 3 total (2 tests, 1 TypeScript style)

Next Steps:

  1. Fix the false positive test at line 66-73 to call onBlockEditorChange instead of directly setting value
  2. Replace direct @Input property assignments with writeValue/setDisabledState CVA calls where testing CVA behavior
  3. Add spectator.detectChanges() after state mutations
  4. Consider detectChanges: false in the factory to control initialization

The production code change itself is safe to merge. The test issues are pre-existing patterns in this file that the PR perpetuates — flagged here so they don't compound further.

🤖 Generated with Claude Code

@github-actions github-actions bot added the Area : Frontend PR changes Angular/TypeScript frontend code label Mar 12, 2026
zJaaal and others added 2 commits March 12, 2026 11:37
- Add void return type to onBlockEditorChange
- Fix false positive test: reset form value then call onBlockEditorChange
- Replace direct disabled property mutations with setDisabledState() CVA method
- Add detectChanges() after state mutations in CSS-class test
- Fix typo ControlValueAccesor -> ControlValueAccessor
- Add comment explaining why characterCount uses optional chaining

Co-authored-by: fmontes <751424+fmontes@users.noreply.github.com>
Copilot AI changed the title Fix: Block editor charCount/wordCount/readingTime missing from API response Fix: Block editor charCount/wordCount/readingTime missing from Page API response Mar 12, 2026
@zJaaal zJaaal marked this pull request as ready for review March 12, 2026 15:41
@zJaaal zJaaal added this pull request to the merge queue Mar 12, 2026
Merged via the queue into main with commit 7bca343 Mar 12, 2026
22 checks passed
@zJaaal zJaaal deleted the copilot/add-read-time-to-api branch March 12, 2026 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Read time visible in Block Editor but not available in Page API

5 participants