Skip to content

fix(uve): lock layout canvas during save to prevent container data loss (#36197) - #37168

Open
gortiz-dotcms wants to merge 4 commits into
mainfrom
issue-36197-layout-canvas-lock-save
Open

fix(uve): lock layout canvas during save to prevent container data loss (#36197)#37168
gortiz-dotcms wants to merge 4 commits into
mainfrom
issue-36197-layout-canvas-lock-save

Conversation

@gortiz-dotcms

@gortiz-dotcms gortiz-dotcms commented Aug 24, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #36197 — UVE Layout tab data loss caused by the GridStack canvas never being locked while a layout save is in flight.

Root cause

Every layout change emits templateChangeupdateTemplate$ → 5-second debounce → POST /api/v1/page/{id}/layout. The canvas remained fully interactive during the entire debounce window, the in-flight POST, and the pageReload() + updateOldRows() re-hydration cycle. Any edit made in that window was silently discarded when the server response overwrote local state. Because the backend performs a destructive diff (deletes any container absent from the incoming payload), mid-flight edits could permanently remove content.


Changes

TemplateBuilderComponent (template-builder.component.ts / .html)

  • @Input() disabled = false — new input that locks the canvas.
  • applyGridDisabled(disabled) — called synchronously from ngOnChanges (before any render cycle):
    • grid.disable() / grid.enable() on the main grid and every subgrid — blocks drag, resize, and external drop at the GridStack event level.
    • Dispatches keydown Escape on document — GridStack's DDDraggable registers a document-level keydown handler and calls cancel() on Escape, removing the drag clone from the body so the overlay is visible. PrimeNG dropdowns also close on Escape, covering the open-dropdown edge case.
  • Visual overlay@if (disabled) block rendered after the grid content (correct DOM stacking order), with bg-white/50 backdrop-blur-[2px] and a centered p-progress-spinner, so the user clearly sees the canvas is saving.

EditEmaLayoutComponent (edit-ema-layout.component.ts / .html)

  • $isSaving computed signalcomputed(() => uveStore.uveStatus() === UVE_STATUS.LOADING), bound as [disabled]="$isSaving()" on the template builder.
  • Lock timing fixedsetUveStatus(LOADING) moved from the tap() before debounceTime (locked on every keystroke) into switchMap (locks only when the POST is actually sent, i.e. when the "Saving…" toast appears). Users can edit freely during the 5-second debounce window.
  • nextTemplateUpdate guard — returns early when uveStatus === LOADING. Covers edge cases where an in-progress action (held drag, open dropdown selection) completes after the freeze activates, preventing the late templateChange from resetting the debounce or corrupting the in-flight payload.

Lock lifecycle

User edits → 5 s debounce settles
  → setUveStatus(LOADING)           [canvas locks + overlay appears]
  → POST /api/v1/page/{id}/layout
  → handleSuccessSaveTemplate()
  → pageReload()                    [LOADING held through re-fetch]
  → updateOldRows() re-hydration
  → setUveStatus(LOADED)            [canvas unlocks]

Test plan

  • Edit layout (move a row) → wait 5 s → "Saving…" toast appears → canvas shows overlay + spinner → cannot drag/add/resize during save + reload cycle → canvas unlocks after reload
  • Edit layout → do not wait → immediately start dragging "Add Row" button → hold for 5 s → Escape cancels the drag, overlay appears → row is not added
  • Edit layout → open "+" container dropdown in a box → wait 5 s → dropdown closes, overlay appears → canvas is frozen
  • Edit layout → save fails (disconnect network) → canvas unlocks, uveStatus set to ERROR, user can retry
  • pnpm nx test portlets-edit-ema-portlet --testPathPattern=edit-ema-layout
  • pnpm nx test template-builder --testPathPattern=template-builder.component

Grabacion.de.pantalla.2026-08-24.a.la.s.5.41.44.p.m.mov

🤖 Generated with Claude Code

…ss (#36197)

The GridStack canvas in the UVE Layout tab was never disabled while a save was
in flight, allowing edits during the debounce window, the in-flight POST, and
the post-save pageReload() re-hydration to be silently dropped when the server
response overwrote local state.

Changes:
- Add `disabled` @input to TemplateBuilderComponent. When true, ngOnChanges
  calls grid.disable() + subgrid.disable() synchronously (before any render
  cycle) and dispatches keydown Escape to cancel any in-progress drag or close
  any open PrimeNG dropdown.
- Add a visible overlay (semi-transparent white + backdrop-blur + PrimeNG
  ProgressSpinner) rendered after the grid content so it sits on top in DOM
  order, giving users clear saving feedback.
- Add $isSaving computed signal in EditEmaLayoutComponent that reads
  uveStatus === LOADING and binds it to [disabled] on the template builder.
- Move setUveStatus(LOADING) from the tap() before debounceTime into switchMap,
  so the canvas only locks when the POST is actually sent — not on every
  keystroke during the 5-second debounce window.
- Guard nextTemplateUpdate() to discard templateChange events that arrive while
  uveStatus === LOADING, covering the edge case where an in-progress action
  (held drag, open dropdown) completes after the freeze activates.

The lock spans the full lifecycle: POST fired → pageReload() re-fetch →
updateOldRows() re-hydration → uveStatus reset to LOADED → canvas unlocked.

Refs: #36197

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Claude finished @gortiz-dotcms's task in 3m 3s —— View job


SDK Compatibility Analysis

  • Read docs/core/SDK_BREAKING_CHANGE_CATEGORIES.md
  • Retrieved the full PR diff (6 files, 196/4 +/-)
  • Analyzed diff against every category (G-1/G-2/G-3, R-1, U-1/U-2, H-1)
  • Determined compatibility impact

Result: No SDK breaking changes detected.

The diff touches only two admin-editor UI libraries:

  • core-web/libs/portlets/edit-ema/portlet/.../edit-ema-layout/* — adds an $isSaving computed signal, a [disabled] binding, and a LOADING-status guard in nextTemplateUpdate().
  • core-web/libs/template-builder/src/lib/components/template-builder/* — adds a disabled @Input(), applyGridDisabled() (GridStack disable()/enable() + a synthetic Escape keydown), and a visual overlay.

None of it touches an SDK-relevant wire surface:

  • No GraphQL query/schema changes (page-api.ts, buildPageQuery) — G-1/G-2/G-3 don't apply.
  • No REST response shape changes to /api/v1/page/*, /api/v1/nav, /api/v1/content — R-1 doesn't apply.
  • No postMessage protocol changes — __DOTCMS_UVE_EVENT__ / DotCMSUVEAction are untouched — U-1/U-2 don't apply.
  • No changes to SdkVersionWebInterceptor, X-DotCMS-Version/X-DotCMS-Min-SDK headers, or compareVersions() — H-1 doesn't apply.

This is purely local Angular editor UI state (GridStack lock/overlay while a layout save is in flight) with no effect on @dotcms/client, @dotcms/react, @dotcms/angular, or @dotcms/uve consumers — matches the "Admin UI only change" / "internal refactor with no wire-format change" non-breaking examples in the reference doc.

No comment or label added, per instructions for non-breaking changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cover the disabled input, overlay rendering, grid API interactions,
nextTemplateUpdate LOADING guard, and debounce-before-lock behaviour.

Refs: #36197
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MockDotRouterJestService is a singleton across the test suite; without
clearing forbidRouteDeactivation before the guard assertion, accumulated
calls from prior tests cause a false failure.

Refs: #36197
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

Layout: Lose containers/content because the editing canvas is never locked during save

1 participant