Skip to content

fix(discussions): emit REQUEST_DISCUSSION_HIGHLIGHTS when iframe CSLPs mutate#583

Open
karancs06 wants to merge 1 commit intodevelop_v4from
fix/discussion-comment-variant-cslp-highlight
Open

fix(discussions): emit REQUEST_DISCUSSION_HIGHLIGHTS when iframe CSLPs mutate#583
karancs06 wants to merge 1 commit intodevelop_v4from
fix/discussion-comment-variant-cslp-highlight

Conversation

@karancs06
Copy link
Copy Markdown
Contributor

@karancs06 karancs06 commented Apr 17, 2026

Problem

When the user switches variants, the iframe's CSR app re-renders asynchronously and mutates every data-cslp attribute. If the visual editor pushes comment-icon highlights optimistically (before those mutations settle), two failure modes follow:

  • Base → Variant: Icons are mounted against the stale base-CSLP element. After the CSR re-render the target element is gone; scroll-tracking falls back to its silent skip path and the icon drifts permanently.
  • Variant → Base: The SDK's internal 15 × 60 ms = 900 ms retry window expires before the iframe finishes reverting to base CSLP values. The icon never appears.

Solution — pull-based, SDK-driven

This PR mirrors the highlight-variant pattern: the MutationObserver lives in the iframe (only place it can live because the iframe may be cross-origin from the visual editor), detects data-cslp changes, and signals the visual editor to re-send the highlight list. The VB owns the field-path list, so the SDK cannot re-mount icons on its own — it can only ask for a refresh.

1. REQUEST_DISCUSSION_HIGHLIGHTS postMessage event (postMessage.types.ts)

REQUEST_DISCUSSION_HIGHLIGHTS = "request-discussion-highlights",

Semantically names what the iframe is saying: "my CSLPs moved, please re-sync highlights."

2. Debounced emit from the existing observer (useRecalculateVariantDataCSLPValues.ts)

updateVariantClasses already sets up a MutationObserver per [data-cslp] element. We add a single fire-and-forget emit, debounced 200 ms so a burst of mutations coalesces into one request:

let highlightsRequestTimer: ReturnType<typeof setTimeout> | null = null;
const requestDiscussionHighlights = () => {
    if (highlightsRequestTimer !== null) clearTimeout(highlightsRequestTimer);
    highlightsRequestTimer = setTimeout(() => {
        highlightsRequestTimer = null;
        visualBuilderPostMessage?.send(
            VisualBuilderPostMessageEvents.REQUEST_DISCUSSION_HIGHLIGHTS
        );
    }, DISCUSSION_HIGHLIGHTS_DEBOUNCE_MS);
};
// ...inside MutationObserver callback after updateElementClasses():
requestDiscussionHighlights();

3. useVariantFieldsPostMessageEvent — SSR path (useVariantsPostMessageEvent.ts)

For SSR pages the DOM is already in its final state when the variant arrives — updateVariantClasses' observer will never fire. Emit the request immediately:

if (isSSR) {
    if (selectedVariant) { addVariantFieldClass(selectedVariant); }
    visualBuilderPostMessage?.send(
        VisualBuilderPostMessageEvents.REQUEST_DISCUSSION_HIGHLIGHTS
    );
} else {
    // CSR: the observer inside updateVariantClasses handles emission.
    updateVariantClasses();
}

4. Orphan cleanup in updateHighlightedCommentIconPosition (generateHighlightedComment.tsx)

Previously, if an icon's field-path no longer matched any [data-cslp] in the DOM (e.g. after a variant switch mutated attributes), the function silently skipped it — the icon stayed visible at its old position and drifted during scroll. Now it's removed immediately:

if (targetElement) {
    icon.style.top = `${top - highlighCommentOffset}px`;
    icon.style.left = `${left - highlighCommentOffset}px`;
} else {
    icon.remove(); // visual editor will re-mount via REQUEST_DISCUSSION_HIGHLIGHTS
}

This is independently correct even without the new event.

Why a pull (not a push)?

An earlier approach had the SDK emit a "DOM settled, proceed" gate event that the visual editor used to unblock a prequeued push. That worked but forced VB to carry cslpSettled state, a 5-second fallback, and an isVariantChangeRef skip-on-mount guard. The pull pattern is stateless on the VB side (just a listener) and self-heals for any cause of CSLP mutation — hydration, dynamic content, route changes — not only variant switches.

Tests

File Coverage
useVariantsPostMessageEvent.spec.ts 3 new tests in the SSR-handling block: emits REQUEST_DISCUSSION_HIGHLIGHTS for SSR+variant, SSR+null variant; does NOT emit directly for CSR (observer handles it)
generateHighlightedComment.test.ts (new file, 5 tests) Position updated when target exists; orphan removal; mixed case; no-throw on empty DOM; removeAllHighlightedCommentIcons

The MutationObserver → debounce → postMessage chain is an integration guarantee — jsdom does not reliably fire attribute mutations, so that end-to-end path is verified in the visual-editor DiscussionPage.spec.tsx tests (companion PR) where the listener is driven directly.

Full suite: 756 tests pass.

Companion PR

Visual editor: contentstack/visual-builder#2102

🤖 Generated with Claude Code

@karancs06 karancs06 requested review from a team as code owners April 17, 2026 07:33
@github-actions
Copy link
Copy Markdown

🔒 Security Scan Results

ℹ️ Note: Only vulnerabilities with available fixes (upgrades or patches) are counted toward thresholds.

Check Type Count (with fixes) Without fixes Threshold Result
🔴 Critical Severity 0 0 10 ✅ Passed
🟠 High Severity 0 0 25 ✅ Passed
🟡 Medium Severity 1 0 500 ✅ Passed
🔵 Low Severity 0 0 1000 ✅ Passed

⏱️ SLA Breach Summary

✅ No SLA breaches detected. All vulnerabilities are within acceptable time thresholds.

Severity Breaches (with fixes) Breaches (no fixes) SLA Threshold (with/no fixes) Status
🔴 Critical 0 0 15 / 30 days ✅ Passed
🟠 High 0 0 30 / 120 days ✅ Passed
🟡 Medium 0 0 90 / 365 days ✅ Passed
🔵 Low 0 0 180 / 365 days ✅ Passed

✅ BUILD PASSED - All security checks passed

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 17, 2026

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 72.2% 8276 / 11462
🔵 Statements 72.2% 8276 / 11462
🔵 Functions 75.28% 329 / 437
🔵 Branches 85.96% 1268 / 1475
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts 10.84% 100% 50% 10.84% 29-33, 37-200
src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts 98.74% 92.1% 100% 98.74% 50-51
src/visualBuilder/generators/generateHighlightedComment.tsx 48.07% 100% 66.66% 48.07% 20-72, 117-121, 149-153, 168
Generated in workflow #794 for commit f03cee9 by the Vitest Coverage Report Action

…EQUEST_DISCUSSION_HIGHLIGHTS)

When the user switches variants, the iframe's CSR app re-renders
asynchronously and mutates every data-cslp attribute. If the visual
editor pushes comment-icon highlights before those mutations settle,
icons anchor to stale CSLP elements and drift permanently, or the
SDK's internal retry window (900 ms) expires before the correct element
appears and no icon is mounted.

This change inverts the coordination: the SDK's existing variant-class
MutationObserver now ALSO emits REQUEST_DISCUSSION_HIGHLIGHTS (debounced
200 ms) whenever data-cslp attributes mutate. The visual editor treats
that as a pull request and re-sends the highlight payload against the
now-final DOM. Because the payload already contains both base and
variant CSLP rows, whichever one matches the current DOM wins.

Changes in this repo:
- postMessage.types.ts: add REQUEST_DISCUSSION_HIGHLIGHTS event.
- useRecalculateVariantDataCSLPValues.ts: debounce-emit the event from
  the existing per-element MutationObserver callback.
- useVariantsPostMessageEvent.ts: for SSR apps the DOM is already final
  so emit immediately; for CSR apps the observer handles it.
- generateHighlightedComment.tsx: updateHighlightedCommentIconPosition
  now removes orphaned icons whose target [data-cslp] has disappeared,
  instead of silently leaving them drifted.

Tests: 3 new SSR-path tests plus a 5-test file for the orphan cleanup
in generateHighlightedComment. The MutationObserver → debounce →
postMessage path is an integration guarantee — jsdom does not fire
attribute mutations reliably, so it is verified in the visual-editor
DiscussionPage tests where the listener is driven directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@karancs06 karancs06 force-pushed the fix/discussion-comment-variant-cslp-highlight branch from e189f93 to f03cee9 Compare April 24, 2026 00:51
@github-actions
Copy link
Copy Markdown

🔒 Security Scan Results

ℹ️ Note: Only vulnerabilities with available fixes (upgrades or patches) are counted toward thresholds.

Check Type Count (with fixes) Without fixes Threshold Result
🔴 Critical Severity 0 0 10 ✅ Passed
🟠 High Severity 0 0 25 ✅ Passed
🟡 Medium Severity 1 0 500 ✅ Passed
🔵 Low Severity 0 0 1000 ✅ Passed

⏱️ SLA Breach Summary

✅ No SLA breaches detected. All vulnerabilities are within acceptable time thresholds.

Severity Breaches (with fixes) Breaches (no fixes) SLA Threshold (with/no fixes) Status
🔴 Critical 0 0 15 / 30 days ✅ Passed
🟠 High 0 0 30 / 120 days ✅ Passed
🟡 Medium 0 0 90 / 365 days ✅ Passed
🔵 Low 0 0 180 / 365 days ✅ Passed

✅ BUILD PASSED - All security checks passed

@karancs06 karancs06 changed the title fix(discussions): emit CSLP_TAGS_UPDATED after variant switch so comment icons mount at correct positions fix(discussions): emit REQUEST_DISCUSSION_HIGHLIGHTS when iframe CSLPs mutate Apr 24, 2026
DATA_CSLP_ATTR_SELECTOR
);
updateElementClasses(element, dataCslp || "", observer);
requestDiscussionHighlights();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

we can debounce it right? I don't want us to spam, postmessages from LP SDK through mutation observer

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, it's already debounced — requestDiscussionHighlights schedules a single 200ms timer (DISCUSSION_HIGHLIGHTS_DEBOUNCE_MS) and resets it on every observed mutation, so a burst of data-cslp mutations from a CSR re-render coalesces into exactly one postMessage. See lines 14–17 (constant) and 47–57 (debounce wrapper) in this file.

If you'd prefer a longer window (say 300ms or 500ms) for extra safety against very chunky React renders, easy to bump.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants