Skip to content

refactor(uve): explicit iframe sizing with DevTools-style canvas controls#35539

Merged
rjvelazco merged 162 commits into
mainfrom
issue-35514-uve-iframe-sizing-phase-1
May 8, 2026
Merged

refactor(uve): explicit iframe sizing with DevTools-style canvas controls#35539
rjvelazco merged 162 commits into
mainfrom
issue-35514-uve-iframe-sizing-phase-1

Conversation

@fmontes
Copy link
Copy Markdown
Member

@fmontes fmontes commented May 2, 2026

Summary

Closes #35514. Replaces the iframe height-observation pipeline with explicit, user-controllable iframe sizing, and rebuilds the overlay, selection, and bounds-sync machinery on top.

The original bug was an infinite growth loop on pages using viewport-relative units (e.g. min-height: 100dvh) — the iframe's height was both an input to and output of layout.


Iframe sizing

  • Iframe width and height are now editor state, not derived measurements.
  • The user controls them via resize handles, numeric W × H inputs, or device presets.
  • Zoom scales what the page inside renders via CSS transform; the iframe's CSS box stays put.
  • Iframe size survives zoom, device switches, and side-panel toggling.
  • Reset button restores 100% zoom and snaps the iframe back to fill the canvas.

Bounds and overlay anchoring

  • Editor used to pull bounds on demand; that races every kind of reflow.
  • SDK now pushes fresh bounds whenever the iframe layout settles, debounced 100ms.
  • One escape hatch (UVE_FLUSH_BOUNDS) for drag/drop's synchronous needs.
  • New $iframeLayoutLocked predicate captures "iframe is mid-flux" in one place.
  • Overlays hide while locked; SET_BOUNDS arrival delivers coords and releases the lock together.
  • Re-anchor logic moved into a dedicated withSelectionAnchor slice.
  • Match key is (inode, container.identifier, container.uuid) to disambiguate duplicate contentlets.

Selection and the hover toolbar

  • Two overlays: hover follows the pointer and owns all action buttons; selected is a persistent border + label, no tools.
  • Visual hint between them: 1px outer line on hover, +1px inset shadow on selected — outer rectangle doesn't grow on click.
  • Pencil button restored to its legacy behavior: opens the full-editor modal with the "all pages / this page" prompt.
  • Pencil is stateless w.r.t. selection — opening the modal doesn't change what's selected or what the side panel shows.
  • Side-panel quick edit moved to its own bolt button next to the pencil.
  • Toolbar order: drag · VTL · bolt · style · pencil · delete, all tooltipped.

Quick-edit form

  • Cancel and Save are always enabled — no more confusing disabled states.
  • Cancel resets the form (if dirty), clears the active contentlet, and closes the panel.
  • Save with an invalid form calls markAllAsTouched() to surface validation errors instead of silently no-op'ing.
  • "Edit in full editor" button moved from the footer to the form's top-right corner — small, with a pencil icon.
  • Footer is now Cancel (left) + Save (right).

Page navigation

  • Clicking a link in the iframe (or switching persona/language) used to unmount the entire editor for the fetch.
  • Cause: pageAssetResponse: null was being written inside the load reset.
  • Fix: keep the previous asset until the new one resolves; chrome stays mounted, content updates in place.
  • Selection state still clears on navigation so it doesn't leak into the new page.

UI consistency

  • The pencil's copy-decision modal and the quick-edit's copy-decision view now share one card UI.
  • Built with plain <button> + Tailwind utilities — no p-button / [dt] / ::ng-deep workarounds.
  • Same icons, same i18n keys across both surfaces.

Architecture doc

  • New libs/portlets/edit-ema/ARCHITECTURE.md covers the contracts above.
  • Bounds sync, reflow lifecycle, three contentlet signals (hovered / selected / panel-target), navigation rule, parent/iframe seam.

Test plan

  • min-height: 100dvh page renders without runaway growth
  • Zoom (10–300%) preserves on-screen iframe size
  • Resize handles clamp to canvas in responsive mode
  • Device presets fit canvas at auto-fit zoom; orientation refit works
  • User-set iframe size survives toggling left/right panels
  • Hover overlay lets wheel/click through; click selects without firing page links
  • Second click on a selected contentlet lets internal links fire
  • Selected overlay re-anchors smoothly on scroll, zoom, device switch, sidebar open/close
  • Pencil opens full-editor modal with copy-decision on multi-page; doesn't change selection
  • Bolt opens side-panel quick-edit; style icon opens style editor
  • Quick-edit Cancel resets form, clears active, closes panel
  • Quick-edit Save with required fields empty surfaces validation
  • Page navigation keeps chrome mounted and clears selection
  • Both side panels open on a 14" laptop without horizontal bleed
  • Palette section-jump scrolls inside the iframe
  • Contentlet tools and dropzone hide during resize and reappear on release

Tests added

  • withView.spec.ts (~30 cases): iframe sizing, device-fit math, orientation refit, exit-preset, zoom reset/snap
  • dot-uve-iframe-resize-handles.component.spec.ts: pointerdown/move/up + destroy-mid-drag
  • dot-uve-zoom-controls.component.spec.ts: option list, change handler
  • dot-uve-contentlet-tools.component.spec.ts: hover overlay rendering + content-type label
  • edit-ema-editor.component.spec.ts: handleSectionOffset regression

Some specs reference symbols renamed or removed during the bounds-channel and binary-selector refactors; cleanup deferred.

🤖 Generated with Claude Code

This PR fixes: #35514

fmontes added 30 commits April 30, 2026 09:49
Phase 1 of #35514. Removes the ResizeObserver/MutationObserver pipeline
that derived the canvas height from the iframe document. Pages with
viewport-relative units on layout containers (e.g. min-height: 100dvh)
caused an infinite growth loop because the iframe's height was both an
input to and an output of layout.

The iframe now has explicit dimensions (viewIframeWidth, viewIframeHeight)
that are user-controlled inputs, defaulting to 1520 x 1080. Content
taller than the iframe scrolls inside the iframe. Inside the iframe,
vh/dvh resolve against a fixed viewport, so there is no feedback loop.

Overlays will glitch on iframe scroll until phase 2 wires scroll-aware
clearing of contentlet bounds.
The iframe element used h-full but its host (<dot-uve-iframe>) had no
height, so 100% resolved against zero. Add w-full h-full to the host
class so it fills .iframe-wrapper.
…-sizing

WIP — more tweaks pending.

Adds DevTools-style explicit iframe sizing:
- viewIframeWidth / viewIframeHeight are now device-preset and user-driven
  (resize handles, numeric inputs), not derived from page content.
- New $viewIsResponsiveMode computed (true when no specific device preset).
- viewSetDevice / viewSetOrientation now set explicit pixel dimensions.
- viewSetIframeSize clamps to MIN_IFRAME_WIDTH / MIN_IFRAME_HEIGHT.

Editor:
- ResizeObserver on .canvas-viewport syncs iframe size to viewport in
  responsive mode. Effect resyncs when user exits a device preset.
- Removed legacy $iframeWrapperStyles override; wrapper always fills the
  store-sized .canvas-inner.
- Initial state width/height = 0; ngAfterViewInit applies real size before
  first paint to avoid scrollbar flash on load.

New components (Tailwind, no SCSS):
- <dot-uve-iframe-resize-handles>: full-length right/bottom bars + corner
  ball. Hidden in device mode. Drag delta divided by zoom.
- <dot-uve-iframe-size-input>: width × height numeric inputs in toolbar.
  Editing while a device is active flips back to responsive mode.
Previously the iframe could grow beyond what the canvas could show at the
current zoom — e.g. drag to 2000px at 50% zoom (visually fits in 1000px),
then zoom back to 100% and the canvas had to scroll.

Now in responsive mode the iframe size is clamped so iframe * zoom never
exceeds the canvas viewport. The clamp re-applies on every zoom change
(viewZoomSetLevel, in/out/reset all delegate to it) and on every drag /
numeric-input update. Device-mode keeps preset dimensions unchanged and
lets the canvas scroll if needed.

Adds viewCanvasAvailableWidth/Height to UVEState and
viewSetCanvasAvailableSize action; the editor's ResizeObserver feeds the
real (padding/gutter-excluded) canvas size into the store.
Reuses the existing scroll plumbing — updateEditorScrollState on pointer
down, updateEditorOnScrollEnd on pointer up — so editorBounds and
editorContentArea clear during the drag and restore on release.
The canvas row uses margin: 0 auto, so growing the iframe shifted the
right/bottom edge by only half the size delta — the cursor visibly
outpaced the handle.

Switch the drag math to measure the handle's current edge each frame
and grow the iframe by the cursor's distance from that edge. The handle
stays under the cursor whether the canvas is centered, left-anchored
after overflow, or transitioning between the two.
Drag and numeric-input updates now cap the iframe so its zoomed size
never exceeds the available canvas — no horizontal/vertical canvas
scrollbars in responsive mode. Device mode is unaffected (preset sizes
keep their dimensions; canvas scrolls if needed).
Switch .browser-url-bar-container from align-items: center to
align-items: stretch so all toolbar items match the tallest one. Drop
the redundant h-full on dot-uve-iframe-size-input — flex stretch
handles it.
- viewZoomSetLevel no longer reshapes the iframe — zooming in past the
  canvas just makes the canvas scroll (DevTools behavior). Size inputs
  stay stable while zooming.
- viewZoomReset now also snaps the iframe to the canvas viewport in
  responsive mode (device mode keeps its preset size). Reset becomes a
  one-click 'fit and zoom 100%'.
The iframe's on-screen size is now always the user-set dimensions
(handles never move with zoom). Zoom changes only the iframe's internal
viewport via inverse scaling: at 70% zoom the iframe is laid out at
dimensions/0.7 CSS pixels and scaled by 0.7 to fill the same on-screen
box. The page inside the iframe sees a larger or smaller viewport
depending on zoom; canvas never scrolls.
viewIframeWidth/Height now represent on-screen size (since zoom adjusts
the iframe's internal viewport, not its on-screen extent). Update both
the resize-handle drag math and the responsive-mode canvas clamp to
work directly in on-screen pixels — no more dividing by zoom — so the
iframe stays inside the canvas at any zoom.
…mode

- Picking a device computes a fit zoom so device dims × zoom fit the
  canvas; iframe gets the on-screen size and viewZoomLevel is set to
  match. Same logic on orientation flip.
- Resize handles now render in device mode too. Dragging from a device
  preset switches back to responsive (clears the device) so the canvas
  clamp and user-driven sizing take over.
The default device left zoom and size untouched, so it inherited values
from a previously-selected device. Picking it now snaps the iframe to
the available canvas viewport at 100% zoom — the expected responsive
default.
The zoom controls now expose a borderless p-select (50/75/100/150/200%)
instead of the discrete +/- buttons. Borders, background, padding and
focus ring are zeroed via [dt] design-token overrides so only the value
and chevron icon remain in the toolbar.
Add size="small" and zero the label's right padding so the value sits
flush against the chevron.
The size input and zoom select share a single rounded gray container
with thin dividers between sections. The reset button moved to the left
of the pill (inside it) since it now resets both zoom and canvas size.
Device presets compute an auto-fit zoom (e.g. 67%) that isn't in the
predefined list, so p-select rendered an empty label. The options list
now appends the current zoom when it isn't a preset, and the bound
value is rounded to match the option label exactly.
- dot-uve-device-controls now renders the device buttons inside
  p-buttongroup; the active device is filled and the rest are outlined.
- Drop the gray pill background from dot-uve-device-controls so the
  group sits flat on the toolbar.
- Tighten left padding on the merged size+zoom pill (the reset button
  brings its own padding).
Lock .browser-toolbar to flex-wrap: nowrap and prevent the start/end
slots from shrinking. The center slot, the URL container, and the URL
pill all get min-width: 0 so the URL bar can compress and ellipsis its
text on narrow toolbars instead of wrapping to a second row.
- Toolbar center now flex+gap and never wraps; uses auto-margin
  centering so its overflow scrolls all the way to the start.
- Browser-toolbar contains its scroll instead of bleeding the layout.
- :host grid uses minmax(0, 1fr) and .editor-content min-width: 0 so
  the editor track shrinks under both side panels at narrow widths.
- Device-controls reverts to plain rounded buttons in a gray pill
  (active = bg-gray-200), no host padding so buttons sit edge-to-edge.
- Round all toolbar p-button icons to text-gray-900 (copy-url, zoom
  reset, orientation, device buttons).
- Browser URL pill gets pr-3; size+zoom select padding zeroed via dt
  and label !p-0.
Use a container query on .p-toolbar-center: under 48rem the URL text
hides and the pill drops its padding/max-width so only the copy-link
icon button remains. Move the pill's right padding from HTML utility
class into SCSS so the query can clear it.
Switch .browser-url-pill from flex: 1 1 0 (always grow) to flex: 0 1 auto
so it sits at its content width up to max-width: 28rem and only shrinks
under toolbar pressure.
Restructure the copy-URL popover so each row stacks the (lighter
gray-700) label above the URL with a small gap, and places the copy
button to the right vertically centered with both. List items get more
breathing room and a divider line beneath each row.
Inject GlobalStore in the editor and append a third entry in
$pageURLS using the current site's hostname (from globalStore.siteDetails)
plus the current page path. Uses the i18n key
uve.toolbar.page.current.site.url.
Move the progress bar out of the toolbar's center template into a
zero-height sibling wrapper. The bar inside is absolutely positioned
just above its slot so it overlays the bottom edge of the toolbar
without pushing the toolbar contents around.
viewSetDevice(DEFAULT_DEVICE) snapped the iframe back to canvas at 100%
zoom, causing a visible jump the moment the user grabbed a resize
handle on a device preview.

Add viewExitDevicePreset that just clears the device flag without
changing size or zoom, and call it from the resize handles after
flipping editorState to SCROLLING — the responsive-mode sync effect
now skips its canvas-snap while scrolling, so the iframe stays
exactly where the user grabbed it.
The active-state check only matched on inode equality, so after exiting
a device preset (viewDevice = null) no button highlighted. Treat a
null device as the desktop preset so the desktop button reflects
'responsive mode' both on explicit selection and on auto-exit.
Switch handle elements from divs to <button> for semantics and a11y,
bump the bar thickness to 1rem (w-4 / h-4), and use a quieter gray
palette: base bg-gray-300, hover bg-gray-500, active bg-gray-600 for
all three handles.
Drop the .edit-panel-wrapper clamp min from 400px to 360px so the
sidebar fits more comfortably alongside the iframe canvas on smaller
displays.
Drop the per-tab utility classes and styleClass overrides on both the
palette and the right sidebar tablist; the design tokens already size
and lay out the tabs. Add a tablist background design token override
(var(--gray-100)) for both.
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 8, 2026

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / GraphQL / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: The public @dotcms/uve SDK contract has changed in ways that affect headless consumers who bundle the npm package independently from the dotCMS server. UVEEventType.REQUEST_BOUNDS was removed from the public enum (replaced by AUTO_BOUNDS + UVE_FLUSH_BOUNDS), and a new DotCMSUVEAction.SET_SELECTED_CONTENTLET action was added that N-1 does not handle. The editor side has a partial dual-emit shim (commit b4cb0ab dual-emits uve-flush-bounds and the legacy uve-request-bounds), which protects the forward direction (new editor → old SDK in the wild). It does not protect the rollback direction:

    1. Old editor emits uve-request-bounds. New SDK only listens for uve-flush-bounds (the legacy alias was removed from onAutoBounds in events.ts). Editor-driven flushes are dropped — drag/drop dropzone bounds become stale.
    2. New SDK's capture-phase click handler calls event.preventDefault() + event.stopPropagation() and emits set-selected-contentlet. N-1 editor has no handler for that action, so the click is consumed but never produces a selection or navigation — clicks inside the iframe become dead.
  • Code that makes it unsafe:

    • core-web/libs/sdk/types/src/lib/editor/public.ts lines ~205-211: UVEEventType.REQUEST_BOUNDS removed; new CONTENTLET_CLICKED, SELECTION_CLEARED, AUTO_BOUNDS event types added; new SET_SELECTED_CONTENTLET action added.
    • core-web/libs/sdk/uve/src/internal/events.ts line 178: onAutoBounds only listens for uve-flush-bounds, not the legacy uve-request-bounds.
    • core-web/libs/sdk/uve/src/internal/events.ts lines ~398-420: New click handler preventDefaults and emits set-selected-contentlet (action that N-1 editor does not process).
    • core-web/libs/portlets/edit-ema/portlet/src/lib/services/iframe-messenger/uve-iframe-messenger.service.ts lines 75-83: Editor dual-emits — covers forward direction only.
  • Note: dotCMS installations that load /ext/uve/dot-uve.js directly via <script> tag are NOT affected — that bundle ships with the dotCMS application and rolls back atomically. Risk is scoped to headless customers who bundle @dotcms/uve from npm independently of the server release cycle.

  • Alternative (if possible): Mirror the editor's back-compat shim on the SDK side for one release: (1) have onAutoBounds also listen for uve-request-bounds so a rolled-back editor's flush requests still resolve; (2) have the SDK's click handler also dual-emit set-contentlet (or check whether the editor responds and fall back to letting the click through) so click-to-select degrades to hover-style selection on N-1 instead of becoming a dead click. Drop both shims in N+1 once N-1 is outside the rollback window.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 8, 2026

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / GraphQL / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: The public @dotcms/uve SDK contract has changed in ways that affect headless consumers who bundle the npm package independently from the dotCMS server. UVEEventType.REQUEST_BOUNDS was removed from the public enum (replaced by AUTO_BOUNDS + UVE_FLUSH_BOUNDS), the corresponding UVEEventPayloadMap entry was deleted, and __UVE_EVENTS__ no longer registers a handler for it. A new DotCMSUVEAction.SET_SELECTED_CONTENTLET action and UVEEventType.CONTENTLET_CLICKED event were added that N-1 does not handle. The editor side has a forward-compat shim (it dual-emits both uve-flush-bounds and the legacy uve-request-bounds), so a new editor + an old SDK in the wild keeps working. The rollback direction is not protected:

    1. Old editor emits uve-request-bounds. New SDK only listens for uve-flush-bounds (onAutoBounds no longer subscribes to the legacy name). Editor-driven flushes are dropped — drag/drop dropzone bounds become stale.
    2. New SDK's capture-phase click handler calls event.preventDefault() + event.stopPropagation() and emits set-selected-contentlet. N-1 editor has no handler for that action, so the click is consumed but never produces a selection or navigation — clicks inside the iframe become dead.
    3. Headless TS consumers that imported UVEEventType.REQUEST_BOUNDS will see compile-time breakage when upgrading the @dotcms/uve types package — the value was removed (not deprecated) from the public enum.
  • Code that makes it unsafe:

    • core-web/libs/sdk/types/src/lib/editor/public.ts lines ~196–211: UVEEventType.REQUEST_BOUNDS removed from the public enum and from UVEEventPayloadMap; new CONTENTLET_CLICKED, SELECTION_CLEARED, AUTO_BOUNDS event types added.
    • core-web/libs/sdk/types/src/lib/editor/public.ts line ~115: New DotCMSUVEAction.SET_SELECTED_CONTENTLET = 'set-selected-contentlet' action that N-1 does not process.
    • core-web/libs/sdk/uve/src/internal/constants.ts lines ~26–30: __UVE_EVENTS__[REQUEST_BOUNDS] registration removed; new AUTO_BOUNDS registration only listens for UVE_FLUSH_BOUNDS.
    • core-web/libs/sdk/uve/src/internal/events.ts line ~178: onAutoBounds only listens for uve-flush-bounds, not the legacy uve-request-bounds.
    • core-web/libs/sdk/uve/src/internal/events.ts lines ~398–420: New click handler preventDefaults and emits set-selected-contentlet (action that N-1 editor does not handle).
    • core-web/libs/portlets/edit-ema/portlet/src/lib/services/iframe-messenger/uve-iframe-messenger.service.ts lines 65–83: Editor dual-emits — covers the forward direction (new editor → old SDK) only.
  • Note: dotCMS installations that load /ext/uve/dot-uve.js directly via <script> tag are NOT affected — that bundle ships with the dotCMS application and rolls back atomically. Risk is scoped to headless customers who bundle @dotcms/uve from npm independently of the server release cycle.

  • Alternative (if possible): Mirror the editor's back-compat shim on the SDK side for one release: (1) keep UVEEventType.REQUEST_BOUNDS in the public enum marked @deprecated (matching the internal UVE_REQUEST_BOUNDS enum, which was correctly retained as deprecated) so headless TS consumers don't see a hard compile break; (2) have onAutoBounds also listen for uve-request-bounds so a rolled-back editor's flush requests still resolve; (3) have the SDK's click handler also dual-emit set-contentlet (or check whether the editor responds and fall back to letting the click through) so click-to-select degrades to hover-style selection on N-1 instead of becoming a dead click. Drop both shims in N+1 once N-1 is outside the rollback window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rjvelazco rjvelazco enabled auto-merge May 8, 2026 16:36
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 8, 2026

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / GraphQL / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: The public @dotcms/uve SDK contract changes in ways that affect headless consumers who bundle the npm package independently from the dotCMS server. UVEEventType.REQUEST_BOUNDS is removed from the public enum (its UVEEventPayloadMap entry is deleted, and __UVE_EVENTS__ no longer registers a handler for it). A new DotCMSUVEAction.SET_SELECTED_CONTENTLET action and UVEEventType.CONTENTLET_CLICKED event are added that N-1 does not handle. The editor side has a forward-compat shim — the messenger dual-emits both uve-flush-bounds (new) and uve-request-bounds (legacy @deprecated) for one release — so a new editor + an old SDK in the wild keeps working. The rollback direction is not protected:

    1. Old editor (N-1) emits uve-request-bounds. New SDK only listens for uve-flush-bounds (onAutoBounds no longer subscribes to the legacy name). Editor-driven flushes are dropped — drag/drop dropzone bounds go stale.
    2. New SDK's capture-phase click handler calls event.preventDefault() + event.stopPropagation() and emits set-selected-contentlet. N-1 editor has no handler for that action, so the click is consumed but never produces a selection or page-link navigation — clicks inside the iframe become dead clicks.
    3. Headless TS consumers who imported UVEEventType.REQUEST_BOUNDS see a compile-time break when upgrading the @dotcms/uve types package — the value is removed (not deprecated) from the public enum.
  • Code that makes it unsafe:

    • core-web/libs/sdk/types/src/lib/editor/public.ts lines ~196–211, ~257–266: UVEEventType.REQUEST_BOUNDS removed from the public enum and from UVEEventPayloadMap; new CONTENTLET_CLICKED, SELECTION_CLEARED, AUTO_BOUNDS event types added.
    • core-web/libs/sdk/types/src/lib/editor/public.ts line ~115: New DotCMSUVEAction.SET_SELECTED_CONTENTLET = 'set-selected-contentlet' action that N-1 does not process.
    • core-web/libs/sdk/uve/src/internal/constants.ts lines ~26–58: __UVE_EVENTS__[REQUEST_BOUNDS] registration removed; new AUTO_BOUNDS registration only listens for UVE_FLUSH_BOUNDS.
    • core-web/libs/sdk/uve/src/internal/events.ts lines ~83–199 (onAutoBounds): only listens for uve-flush-bounds, not the legacy uve-request-bounds.
    • core-web/libs/sdk/uve/src/internal/events.ts lines ~365–446 (onContentletClicked): capture-phase click handler preventDefaults and emits set-selected-contentlet (action that N-1 editor does not handle).
    • core-web/libs/portlets/edit-ema/portlet/src/lib/services/iframe-messenger/uve-iframe-messenger.service.ts lines 65–83: editor dual-emits — covers the forward direction (new editor → old SDK) only.
  • Note: dotCMS installations that load /ext/uve/dot-uve.js directly via <script> tag are NOT affected — that bundle ships with the dotCMS application and rolls back atomically. The risk is scoped to headless customers who bundle @dotcms/uve from npm independently of the server release cycle.

  • Alternative (if possible): Mirror the editor's back-compat shim on the SDK side for one release: (1) keep UVEEventType.REQUEST_BOUNDS in the public enum marked @deprecated (matching the internal UVE_REQUEST_BOUNDS enum, which is correctly retained as deprecated) so headless TS consumers don't see a hard compile break; (2) have onAutoBounds also listen for uve-request-bounds so a rolled-back editor's flush requests still resolve; (3) have the SDK's click handler also dual-emit set-contentlet (or fall back to letting the click through when no selection-cleared round-trip is observed) so click-to-select degrades to hover-style selection on N-1 instead of becoming a dead click. Drop both shims in N+1 once N-1 is outside the rollback window.

@rjvelazco rjvelazco enabled auto-merge May 8, 2026 17:10
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 8, 2026

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / GraphQL / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: The public @dotcms/uve SDK contract changes in ways that affect headless consumers who bundle the npm package independently from the dotCMS server. UVEEventType.REQUEST_BOUNDS is removed from the public enum (its UVEEventPayloadMap entry is deleted, and __UVE_EVENTS__ no longer registers a handler for it). A new DotCMSUVEAction.SET_SELECTED_CONTENTLET action and UVEEventType.CONTENTLET_CLICKED event are added that N-1 does not handle. The editor side has a forward-compat shim — the messenger dual-emits both uve-flush-bounds (new) and uve-request-bounds (legacy @deprecated) for one release — so a new editor + an old SDK in the wild keeps working. The rollback direction is not protected:

    1. Old editor (N-1) emits uve-request-bounds. New SDK only listens for uve-flush-bounds (onAutoBounds no longer subscribes to the legacy name). Editor-driven flushes are dropped — drag/drop dropzone bounds go stale.
    2. New SDK's capture-phase click handler calls event.preventDefault() + event.stopPropagation() and emits set-selected-contentlet. N-1 editor has no handler for that action, so the click is consumed but never produces a selection or page-link navigation — clicks inside the iframe become dead clicks.
    3. Headless TS consumers who imported UVEEventType.REQUEST_BOUNDS see a compile-time break when upgrading the @dotcms/uve types package — the value is removed (not deprecated) from the public enum.
  • Code that makes it unsafe:

    • core-web/libs/sdk/types/src/lib/editor/public.ts lines ~205–211, ~257–266: UVEEventType.REQUEST_BOUNDS removed from the public enum and from UVEEventPayloadMap; new CONTENTLET_CLICKED, SELECTION_CLEARED, AUTO_BOUNDS event types added.
    • core-web/libs/sdk/types/src/lib/editor/public.ts line ~122: New DotCMSUVEAction.SET_SELECTED_CONTENTLET = 'set-selected-contentlet' action that N-1 does not process.
    • core-web/libs/sdk/uve/src/internal/constants.ts lines ~26–58: __UVE_EVENTS__[REQUEST_BOUNDS] registration removed; new AUTO_BOUNDS registration only listens for UVE_FLUSH_BOUNDS.
    • core-web/libs/sdk/uve/src/internal/events.ts line ~178 (onAutoBounds): only listens for uve-flush-bounds, not the legacy uve-request-bounds.
    • core-web/libs/sdk/uve/src/internal/events.ts lines ~365–446 (onContentletClicked): capture-phase click handler preventDefaults and emits set-selected-contentlet (action that N-1 editor does not handle).
    • core-web/libs/portlets/edit-ema/portlet/src/lib/services/iframe-messenger/uve-iframe-messenger.service.ts lines 65–83: editor dual-emits — covers the forward direction (new editor → old SDK) only.
  • Note: dotCMS installations that load /ext/uve/dot-uve.js directly via <script> tag are NOT affected — that bundle ships with the dotCMS application and rolls back atomically. The risk is scoped to headless customers who bundle @dotcms/uve from npm independently of the server release cycle.

  • Alternative (if possible): Mirror the editor's back-compat shim on the SDK side for one release: (1) keep UVEEventType.REQUEST_BOUNDS in the public enum marked @deprecated (matching the internal UVE_REQUEST_BOUNDS enum, which is correctly retained as deprecated) so headless TS consumers don't see a hard compile break; (2) have onAutoBounds also listen for uve-request-bounds so a rolled-back editor's flush requests still resolve; (3) have the SDK's click handler also dual-emit set-contentlet (or fall back to letting the click through when no selection-cleared round-trip is observed) so click-to-select degrades to hover-style selection on N-1 instead of becoming a dead click. Drop both shims in N+1 once N-1 is outside the rollback window.

@rjvelazco rjvelazco added this pull request to the merge queue May 8, 2026
Merged via the queue into main with commit dfdeb9a May 8, 2026
52 checks passed
@rjvelazco rjvelazco deleted the issue-35514-uve-iframe-sizing-phase-1 branch May 8, 2026 19:37
@dsilvam dsilvam linked an issue May 11, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Not Safe To Rollback Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code Area : SDK PR changes SDK libraries

Projects

Status: No status

5 participants