@zambon-dev/framework 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -23,6 +23,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
23
23
 
24
24
  ### ⚠ Breaking Changes / Migration
25
25
 
26
+ ## [1.4.0] - 2026-09-16
27
+
28
+ ### Changed
29
+
30
+ - **`framework-button-filters` no longer submits the labels of catalog selections.** A filters
31
+ form reads its values straight off the form, so the display control `lib-catalog-select` adds
32
+ for its own use went to the backend beside the real filters — a request filtering by employee
33
+ carried `employeeName: "753 - ADEMILSON LOPES MAGALHAES"` next to `employeeID: 125`.
34
+
35
+ They were ignored, being filters no service declares, but the day one does filter by a name it
36
+ would receive the formatted label rather than the stored value and quietly match nothing.
37
+
38
+ The labels are still **kept** for the modal: reopening it patches them back into the form, and a
39
+ catalog select backed by a `searchEndpoint` cannot recover its text from the identifier alone —
40
+ it only resolves a display out of a local entries list. Dropping them from what is stored would
41
+ have left the field showing a selection with nothing written in it.
42
+
43
+ ### ⚠ Breaking Changes / Migration
44
+
45
+ - **Upgrade `@zambon-dev/library` together with this release.** The peer range moved from
46
+ `^1.0.0` to `^1.6.0`, because `framework-button-filters` now imports `DisplayControls` from it.
47
+ On an older library that export does not exist, and the filters button fails — at build time if
48
+ the bundler checks exports, otherwise the first time a filter is submitted. The old range would
49
+ have let npm resolve that combination without a word of warning.
50
+
51
+ - **A backend no longer receives the label of a catalog selection.** If one of your services
52
+ filters by a key that a `lib-catalog-select` uses as its `displayControlName` — the `xxxName`
53
+ that comes paired with an `xxxID` — that filter now arrives empty and the query stops narrowing,
54
+ silently returning more rows than before rather than failing.
55
+
56
+ To check, take each filters form and, for every `lib-catalog-select` in it, note the
57
+ `displayControlName`. Then look for a `TryFilter` on that name in the service behind the list.
58
+ Any hit has to move to the identifier instead: the label was never the stored value — it is what
59
+ the catalog chose to display, so filtering by it was already matching on formatting.
60
+
61
+ ## [1.3.1] - 2026-09-10
62
+
63
+ ### Fixed
64
+
65
+ - **Returning to an already-open tab no longer crashes with `Maximum call stack size exceeded`.**
66
+ It affected any screen whose route nests two empty-path levels — the shape every list screen uses:
67
+ `path: ''` with `DefaultTabViewComponent`, and a child `path: ''` with the list component.
68
+
69
+ `CustomReuseStrategy` keyed its detached-view cache on `route.component.name`, and a production
70
+ build renames every class: esbuild wraps each component as `X = (() => { class i { } return i; })()`,
71
+ so the name is one mangled letter that the whole chunk shares. An empty path contributes no URL
72
+ segment, so both levels also resolved to the same URL — which left the two cache keys identical.
73
+ Storing one handle overwrote the other, both route levels were then handed the same detached view,
74
+ and Angular blew the stack building a router state whose node was its own descendant.
75
+
76
+ The key is now built from the component's **identity** and the route's depth rather than its name.
77
+ Applications need to change nothing. Worth knowing when reading old reports of this crash: it only
78
+ ever reproduced in a minified build — `ng serve` keeps real class names — and only on the screens
79
+ whose chunk happened to mangle to the same letter as the framework's, so a rebuild could move the
80
+ symptom from one screen to another.
81
+
26
82
  ## [1.3.0] - 2026-07-30
27
83
 
28
84
  ### Added
@@ -121,7 +177,9 @@ None.
121
177
  available via [GitHub Releases](https://github.com/RicardoZambon/ZLibraries/releases) and the
122
178
  `framework-v*` tags.
123
179
 
124
- [Unreleased]: https://github.com/RicardoZambon/ZLibraries/compare/framework-v1.3.0...HEAD
180
+ [Unreleased]: https://github.com/RicardoZambon/ZLibraries/compare/framework-v1.4.0...HEAD
181
+ [1.4.0]: https://github.com/RicardoZambon/ZLibraries/releases/tag/framework-v1.4.0
182
+ [1.3.1]: https://github.com/RicardoZambon/ZLibraries/releases/tag/framework-v1.3.1
125
183
  [1.3.0]: https://github.com/RicardoZambon/ZLibraries/releases/tag/framework-v1.3.0
126
184
  [1.2.1]: https://github.com/RicardoZambon/ZLibraries/releases/tag/framework-v1.2.1
127
185
  [1.2.0]: https://github.com/RicardoZambon/ZLibraries/releases/tag/framework-v1.2.0
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { inject, ApplicationRef, Inject, Injectable, Component, ViewChild, Input, EventEmitter, Output, NgModule, forwardRef, ChangeDetectorRef, InjectionToken } from '@angular/core';
3
3
  import * as i1$2 from '@zambon-dev/library';
4
- import { RibbonGroupChild, RibbonButtonComponent, DataProviderService, FormService, ModalComponent, DataGridDataset, MultiSelectResultDataset, RouterFormatter, MultiEditorComponent, MultiEditorDataset, MultiSelectComponent, RibbonComponent, GroupContainerComponent, RibbonGroupComponent, DataGridConfigsProvider } from '@zambon-dev/library';
4
+ import { RibbonGroupChild, RibbonButtonComponent, DataProviderService, FormService, ModalComponent, DataGridDataset, DisplayControls, MultiSelectResultDataset, RouterFormatter, MultiEditorComponent, MultiEditorDataset, MultiSelectComponent, RibbonComponent, GroupContainerComponent, RibbonGroupComponent, DataGridConfigsProvider } from '@zambon-dev/library';
5
5
  export { DateValidators } from '@zambon-dev/library';
6
6
  import { ReplaySubject, take, map, switchMap, of, Subject, takeUntil, filter, skip, tap, throwError } from 'rxjs';
7
7
  import { __decorate } from 'tslib';
@@ -121,6 +121,8 @@ let CustomReuseStrategy = class CustomReuseStrategy {
121
121
  tabService;
122
122
  applicationRef = inject(ApplicationRef);
123
123
  cachedHandles = {};
124
+ componentIDs = new WeakMap();
125
+ nextComponentID = 1;
124
126
  //#endregion
125
127
  //#region Properties
126
128
  //#endregion
@@ -193,8 +195,39 @@ let CustomReuseStrategy = class CustomReuseStrategy {
193
195
  if (this.clones[url]) {
194
196
  url = this.clones[url];
195
197
  }
196
- const componentName = route.component?.name ?? '';
197
- return `${url}-${componentName}`;
198
+ // The component is identified by identity, never by name. A production build renames every
199
+ // class: esbuild wraps each component as `X = (() => { class i { } return i; })()`, so
200
+ // `component.name` is a single mangled letter that the whole chunk shares. Nested empty-path
201
+ // routes already resolve to the same URL, because an empty path contributes no segment, so a
202
+ // key built from the name collapsed a screen's tab view and its list into one entry: storing
203
+ // one overwrote the other, both levels were then handed the same detached view, and Angular
204
+ // blew the stack building a router state whose node was its own descendant. That reproduced
205
+ // only in a minified build, and only on the screens whose chunk happened to mangle to the
206
+ // same letter as the framework's.
207
+ //
208
+ // The depth is part of the key too, so two sibling routes sharing a component type under one
209
+ // URL cannot collide either.
210
+ return `${url}-${this.getRouteDepth(route)}-${this.getComponentID(route.component)}`;
211
+ }
212
+ getComponentID(component) {
213
+ if (typeof component !== 'function') {
214
+ return 0;
215
+ }
216
+ let componentID = this.componentIDs.get(component);
217
+ if (componentID === undefined) {
218
+ componentID = this.nextComponentID++;
219
+ this.componentIDs.set(component, componentID);
220
+ }
221
+ return componentID;
222
+ }
223
+ getRouteDepth(route) {
224
+ let depth = 0;
225
+ let current = route.parent;
226
+ while (current) {
227
+ depth++;
228
+ current = current.parent;
229
+ }
230
+ return depth;
198
231
  }
199
232
  getUrlFromRoute(route) {
200
233
  const segments = [];
@@ -1430,14 +1463,35 @@ class ButtonFiltersComponent extends BaseButton {
1430
1463
  //#endregion
1431
1464
  //#region Public methods
1432
1465
  setFilters(filters) {
1466
+ // Kept whole, because reopening the modal patches this straight back into the form and a
1467
+ // catalog select backed by a search endpoint cannot recover its label from the identifier --
1468
+ // it only resolves a display from a local entries list. Dropping the labels here would leave
1469
+ // the field showing a selection with no text.
1433
1470
  this.filters = filters;
1434
1471
  if (this.hasFiltersApplied) {
1435
- this.gridDataset.setFilters(filters);
1472
+ this.gridDataset.setFilters(this.withoutDisplayControls(filters));
1436
1473
  }
1437
1474
  else {
1438
1475
  this.gridDataset.setFilters();
1439
1476
  }
1440
1477
  }
1478
+ //#endregion
1479
+ //#region Private methods
1480
+ /**
1481
+ * Drops the controls that only hold a catalog selection's label.
1482
+ *
1483
+ * They are half of how `lib-catalog-select` works and are created by it, so a screen never asked
1484
+ * for them and a backend has no filter behind them. Sent anyway they are dead weight, and worse:
1485
+ * the day a service does filter by a name, it would receive the formatted label rather than the
1486
+ * stored value and quietly match nothing.
1487
+ *
1488
+ * @param filters The filters as submitted.
1489
+ * @returns The filters a backend should receive.
1490
+ */
1491
+ withoutDisplayControls(filters) {
1492
+ return Object.fromEntries(Object.entries(filters)
1493
+ .filter(([key]) => !DisplayControls.isDisplayControl(this.formGroup.form.get(key))));
1494
+ }
1441
1495
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.6", ngImport: i0, type: ButtonFiltersComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1442
1496
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.6", type: ButtonFiltersComponent, isStandalone: true, selector: "framework-button-filters", inputs: { modalSize: "modalSize", modalTitle: "modalTitle", validateFormFunction: "validateFormFunction" }, providers: [
1443
1497
  FormService,