Skip to content

fix(content-drive): truncate long-text field values in listing rows (#37185) - #37396

Open
ihoffmann-dot wants to merge 6 commits into
mainfrom
issue-37185-content-drive-listing-longtext-projection-impl
Open

fix(content-drive): truncate long-text field values in listing rows (#37185)#37396
ihoffmann-dot wants to merge 6 commits into
mainfrom
issue-37185-content-drive-listing-longtext-projection-impl

Conversation

@ihoffmann-dot

@ihoffmann-dot ihoffmann-dot commented Sep 4, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds TransformOptions.LONG_TEXT_PREVIEW (declared after STORY_BLOCK_VIEW/JSON_VIEW so EnumSet iteration order runs it last) backed by a new LongTextPreviewStrategy.
  • Replaces WYSIWYG/TextArea (Jsoup-extracted plain text) and Story Block (new recursive JSON-tree traversal) field values in a listing row with a ≤150-character preview, instead of the full raw value.
  • Wired opt-in only at BrowserAPIImpl#dotContentMap via a new DotTransformerBuilder#longTextPreview() chain method — never added to defaultOptions, so no other transformer consumer (Content Editor, ContentResource, GraphQL, asset picker) is affected (AC-007).
  • Bug found and fixed while writing the AC-008 test: if a content type's title-source field is itself WYSIWYG/TextArea (its variable is literally title), the strategy would have overwritten the already-correct, untruncated title COMMON_PROPS computes. Fixed by explicitly skipping the title key.
  • Updates the @Schema description on ContentDriveResource#search (AC-005; endpoint is @Hidden, no openapi.yaml regen needed) and removes a dead item.body assertion in the Postman collection (AC-006, a listing row never carried that key).

Test plan

Branched off the approved spec branch per this repo's Spec-Kit flow (spec.md-only in PR1, not merged to main yet).

🤖 Generated with Claude Code

This PR fixes: #37185

Verification (2026-09-04, local)

  • 12/12 unit tests (LongTextPreviewStrategyTest) pass.
  • 45/45 integration tests (BrowserAPITest) pass.
  • 87/87 Postman assertions (ContentDriveResource collection) pass.

Bugs found and fixed in the test code along the way (not the production fix): a package-private field access across packages (fixed via reflection), a test title exceeding the contentlet.title column's varchar(255) limit, an incorrect expected key name (__icon__ vs. the real icon) plus two File-Asset-only keys (mimeType/extension) wrongly expected on a generic-Content row.

Red confirmed: LongTextPreviewStrategy/TransformOptions.LONG_TEXT_PREVIEW
don't exist yet, so the test class doesn't compile.
…37185)

Adds TransformOptions.LONG_TEXT_PREVIEW (declared after STORY_BLOCK_VIEW/
JSON_VIEW so EnumSet order runs it last) backed by LongTextPreviewStrategy,
which replaces WYSIWYG/TextArea/Story Block values with a <=150-char
extracted plain-text preview. Wired opt-in only at
BrowserAPIImpl#dotContentMap via a new DotTransformerBuilder#longTextPreview()
chain method -- never added to defaultOptions, so no other transformer
consumer is affected.
…preview (#37185, AC-008)

If a content type's title-source field is itself WYSIWYG/TextArea (its
variable is literally 'title'), LongTextPreviewStrategy would match it by
field type and clobber the value COMMON_PROPS already populated from
Contentlet#getTitle() with a truncated/HTML-stripped preview. Skip the
'title' key explicitly.
…w trim (#37185)

T030-T033: generic-Content row shape from getPaginatedContents (Drive) and
getFolderContent (Site Browser) both carry AC-002's required keys and
reduced long-text values; Show In List (AC-003) renders a readable
preview; a WYSIWYG title-source field (AC-008) keeps an untruncated title.

Also: AC-005 Schema description update on ContentDriveResource#search
(endpoint is @hidden, no openapi.yaml regen needed), and AC-006 Postman
fix removing the dead item.body read (a listing row never carried that
key) in favor of an assertion that actually runs.
@claude

claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Claude finished @ihoffmann-dot's task in 2m 27s —— View job


Code Review — long-text preview for Content Drive listings

  • Read LongTextPreviewStrategy, TransformOptions, DotTransformerBuilder, StrategyResolverImpl, BrowserAPIImpl wiring
  • Verified strategy ordering (EnumSet ordinal order → runs last)
  • Checked interaction with StoryBlockViewStrategy / JSONViewStrategy
  • Reviewed null/empty and surrogate-pair edge handling

I verified the core correctness claim end-to-end: DotTransformerBuilder.build() passes EnumSet.copyOf(optionsHolder), DotContentletTransformerImpl stores that reference directly (line 71) and iterates it in resolveStrategies — so ordinal order holds and LONG_TEXT_PREVIEW (declared last) runs after STORY_BLOCK_VIEW/JSON_VIEW. I also confirmed JSONViewStrategy only touches JSONField fields, so it doesn't re-serialize the story-block Map back to a string before the preview runs. The defaultOptions set includes STORY_BLOCK_VIEW, so the story-block value is already a Map by the time extractStoryBlockPreview sees it. The design is sound and the opt-in is correctly isolated to the BrowserAPIImpl#dotContentMap call site.

New Issues

  • 🟡 Medium: LongTextPreviewStrategy.java:73-77applyPreview unconditionally calls map.put(field.variable(), ...) for every matching field, even when the field key is absent from the map. When map.get(...) returns null, all three extractors return StringPool.BLANK, so a field that had no value (and thus no key in the transformed map) gets a new "" key inserted into the listing row. This is a subtle response-shape change: rows for content with an empty WYSIWYG/TextArea/Story-Block field would newly carry an empty-string key.
    • Assumption: DefaultTransformStrategy may or may not include unset fields in the map — if it always emits "" for these field types, this is a no-op and can be ignored.
    • What to verify: for a Content Type with an empty WYSIWYG/Story-Block field, does the listing row change from "no key" to key: ""? If BrowserAPITest already asserts on such a row, it's covered; otherwise consider guarding with if (map.containsKey(field.variable())) before overwriting. Fix this →

Everything else checks out:

  • Surrogate-pair guard in truncate (lines 151-153) is correct — a high surrogate at index 149 cuts at 149, and a high surrogate landing at index 150 is naturally excluded by substring(0, 150), so no lone surrogate can be emitted.
  • collectText short-circuits at MAX_PREVIEW_LENGTH before recursing/appending, avoiding full traversal of large story blocks.
  • AC-008 title-key skip (line 72) correctly protects the COMMON_PROPS-computed title.
  • Per-field failures are caught via Try.run(...).onFailure(...) and logged with Logger.warn — no swallowed exceptions, no System.out.
  • No SQL, no new external calls, no permission-surface change; opt-in is not added to defaultOptions, so Content Editor / ContentResource / GraphQL / asset picker are unaffected (AC-007 verified).

No blocking issues. The one Medium is non-blocking and may already be a no-op depending on DefaultTransformStrategy behavior.
· branch issue-37185-content-drive-listing-longtext-projection-impl

…gyTest/BrowserAPITest (#37185)

- defaultOptions_neverIncludesLongTextPreview referenced
  DotContentletTransformerImpl.defaultOptions directly across packages
  (...transform vs. this test's ...transform.strategy) -- the field is
  package-private, so it doesn't compile. Read it via reflection instead.
- The wysiwygTitleField test's long title HTML (400+ chars) exceeded the
  contentlet.title column's varchar(255) limit. Reduced while keeping
  the stripped plain text well over the 150-char preview bound.
- REQUIRED_LISTING_KEYS listed the actual icon key as '__icon__'
  (it's 'icon') and included mimeType/extension, which are File
  Asset-specific and legitimately absent on a generic-Content row.
Base automatically changed from issue-37185-content-drive-listing-longtext-projection to main September 5, 2026 03:56
@ihoffmann-dot ihoffmann-dot self-assigned this Sep 5, 2026
…ate-pair split (#37185)

Found in code review (claude[bot] on PR #37396):
- collectText walked the entire story-block tree before truncate() threw
  away everything past 150 chars -- full-payload work on the exact path
  this feature exists to keep cheap. Short-circuit once enough text is
  collected.
- truncate() could split a UTF-16 surrogate pair (emoji, some CJK) at
  the 150-char boundary, leaving a lone high surrogate. Back off one
  char when the boundary char is a high surrogate.
@github-actions github-actions Bot added the Area : Backend PR changes Java/Maven backend code label Sep 8, 2026
@nollymar nollymar added the PR : dotbot review Trigger dotbot AI code review on this PR label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

dotbot code review:

  • Reviewer: meta/muse-spark-1.3 (medium)
  • Overall: patch is correct
  • New findings this run: 0
  • Prior unresolved dotbot findings still relevant: 0
  • Active findings total: 0

Opt-in LONG_TEXT_PREVIEW correctly runs last via enum ordinal order, is isolated to dotContentMap, and handles HTML/Story Block truncation with title-key protection and surrogate safety. No P0/P1 defect with concrete repo evidence was found.

Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads.

reviewed by dotbot · meta/muse-spark-1.3 · medium

// the content type's title-source field is itself WYSIWYG/TextArea/Story Block.
.filter(field -> !TITTLE_KEY.equals(field.variable()))
.forEach(field -> Try.run(() ->
map.put(field.variable(), extractor.apply(map.get(field.variable()))))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LongTextPreviewStrategy.java:74 absent field keys get inserted as empty strings

Current code:

fields.stream()
    .filter(field -> !TITTLE_KEY.equals(field.variable()))
    .forEach(field -> Try.run(() ->
        map.put(field.variable(), extractor.apply(map.get(field.variable()))))

Problem: Fields absent from the map (unset value) get a new "" key inserted, changing row shape.

Fix:

    .forEach(field -> Try.run(() -> {
        if (map.containsKey(field.variable())) {
            map.put(field.variable(), extractor.apply(map.get(field.variable())));
        }
    }))

What to verify: a Content Type with an empty WYSIWYG/Story Block field whose key is absent from Contentlet#getMap(); the listing row currently gains key: "".

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

dotbot code review:

  • Reviewer: ~z-ai/glm-latest (medium)
  • Overall: patch is incorrect
  • New findings this run: 1
  • Prior unresolved dotbot findings still relevant: 0
  • Active findings total: 1

The opt-in LONG_TEXT_PREVIEW strategy is correctly ordered last via enum ordinal, isolated to the dotContentMap call site, and handles HTML/Story Block truncation, the title-key collision, and surrogate pairs safely. The only residual issue is a low-severity possible empty-string key insertion for unset fields.

Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads.

reviewed by dotbot · ~z-ai/glm-latest · medium

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Backend PR changes Java/Maven backend code PR : dotbot review Trigger dotbot AI code review on this PR

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Content Drive: listing payload carries long-text field values the grid never renders

2 participants