fix(template-builder): stop echoing layout changes synchronously in templates portlet#35698
Conversation
…emplates portlet The templates portlet's wrapper called `updateTemplate.emit(item)` on every `templateChange` from the designer. That propagated through the parent store's `working` signal and echoed straight back into `[layout]` -> `ngOnChanges` -> `updateOldRows` while state still had stale y values from `removeRow` (gaps like [0, 2] before `moveRow` compacts to [0, 1]). `updateOldRows` then matched oldRows by y and silently produced rows with `children: undefined` and missing x/y/w/h fields, manifesting as columns reordering or rows duplicating after a row deletion. The latent bug was masked until commit ab89c85 switched `@for` tracking from `identify(row)` (id+index) to `row.id`. Before that change, removing a row shifted indices and forced Angular to destroy/recreate every subsequent row, which incidentally reset gridstack state. Mirror the EMA pattern (edit-ema-layout.component.ts) — layout only flows back to the designer after a successful save (`onSaveTemplate` updates both `working` and `original`). The existing 5s debounce on `templateUpdate$` already drives auto-save, and `forbidRouteDeactivation()` continues to block navigation immediately on every edit. Added a regression test that fires two rapid `templateChange` emissions (simulating removeRow -> moveRow) and asserts `updateTemplate` is never emitted synchronously, while a single debounced `save` fires with the latest item. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @zJaaal's task in 3m 4s —— View job ReviewThe fix is targeted, the comment is excellent, and the EMA analogy is the right one. A few things worth flagging before merge — none are blockers but the first two are real and the PR currently soft-pedals them in prose only. 1. The bug isn't fixed, only narrowed — and the comment admits it
Concrete edge case: This is unlikely in practice (clicks and Angular CD are serialized) but it's a structural footgun that the prose argument doesn't actually rule out. Suggestion: either (a) fix 2. Regression test's "assertion (2)" is mostly a tautologyThe spec at lines 245-299 asserts If you want (2) to actually be load-bearing, the test needs a host component that re-binds 3.
|
…ert layout ref stability Review feedback on #35698: 1. Updated the comment in onTemplateItemChange to acknowledge the residual save-time echo path: store/dot-template.store.ts:199-202 calls updateWorkingTemplate(template) synchronously inside saveTemplate before the HTTP request, so [item]="vm.working" → [layout] does still flow back through the lib at save-emit time. This is narrowed (only fires after a 5s idle window when the lib has settled) but not eliminated — added a note so a future refactor that fires save on a non-debounced event will notice. 2. Added a breadcrumb to template-builder.component.ts ngOnChanges so the downstream corruption site is one search away. 3. Strengthened the regression test with a second assertion: the lib's [layout] input reference must NOT change across the two rapid templateChange emissions. The original "updateTemplate.emit not called" check is a wrapper-contract test; the new one is a bug-shape test. A refactor that re-introduces an indirect echo path (e.g. moving the update logic into the parent's (templateChange) handler) would still pass the contract assertion but fail this one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Closes #35697.
updateTemplate.emit(item)in the templates portlet wrapper (dot-template-builder.component.ts). It was echoing layout changes synchronously back into the designer's[layout]input, triggeringupdateOldRowsmid-cycle while state had gaps iny(e.g.[0, 2]right afterremoveRow, before grid auto-compact +moveRowcollapse to[0, 1]). That mismatch silently produced rows withchildren: undefinedand missingx/y/w/h— visible as columns reordering or rows duplicating after a row deletion.edit-ema-layout.component.ts): layout only flows back to the designer after a successful save (onSaveTemplateupdates bothworkingandoriginal). Auto-save still fires via the existing 5stemplateUpdate$debounce;forbidRouteDeactivation()continues to block navigation on every edit.libs/template-builder) is untouched — root cause was in the consumer.Why this regressed now
Commit
ab89c85e10(PR #35277) switched@forrow tracking fromidentify(row)(id+index) torow.id. Before that change, removing a row shifted indices and forced Angular to destroy/recreate every subsequent row, which incidentally reset GridStack state and masked this latentupdateOldRowscorruption. EMA was always immune because its layout signal only changes after save.Test plan
Demo
Screen.Recording.2026-05-13.at.5.04.38.PM.mov
Manual
/templates/edit/<id>, ≥ 3 rows with non-zero-x columns.Automated
dot-template-builder.component.spec.tsfires two rapidtemplateChangeemissions (simulatingremoveRow→moveRow) and assertsupdateTemplate.emitis never called synchronously, while a single debouncedsave.emitfires with the latest item.should react to templateChange outputupdated to match the new contract.yarn nx test dotcms-ui— 2129 passed.🤖 Generated with Claude Code