@zambon-dev/shared 1.2.0 → 2.1.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,197 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
23
23
 
24
24
  ### ⚠ Breaking Changes / Migration
25
25
 
26
+ ## [2.1.0] - 2026-09-08
27
+
28
+ ### Added
29
+
30
+ - **External sidebar destinations are now opened, in either of two modes.** `MainLayoutComponent`
31
+ subscribes to `@zambon-dev/library`’s new `SidebarService.menuExternalUrlSelected`: an item whose
32
+ `openMode` is `ExternalNewTab` opens in a new browser tab (`noopener,noreferrer`), and one marked
33
+ `ExternalEmbedded` opens an application tab that displays the destination in a sandboxed iframe,
34
+ keeping the user inside the application. Internal routes are untouched.
35
+
36
+ - **`ExternalUrlResolverService`** — substitutes the runtime placeholders in an external menu URL
37
+ and vets the result. The placeholder set is closed and case-sensitive: `{email}`, `{language}`,
38
+ `{userId}`, `{userName}`. Every substituted value is `encodeURIComponent`-ed, so a name containing
39
+ `&` cannot inject a query parameter — which also means one placeholder must occupy one whole path
40
+ segment or query-parameter value. A supported placeholder with no value becomes an empty string
41
+ (with a console warning); an unrecognized `{…}` is left exactly as configured, so a report URL
42
+ that legitimately contains braces, such as `?filter={"a":1}`, is not corrupted.
43
+
44
+ No authentication token is ever substituted, and the set is closed by construction rather than
45
+ reflected off the stored user info — `AuthenticationService` persists the whole sign-in response
46
+ under `userInfo`, tokens included, so a reflective implementation would let a URL configured as
47
+ `?t={token}` hand the JWT to a third party.
48
+
49
+ `{language}` and `{userName}` work today. `{userId}` and `{email}` resolve to an empty string
50
+ until your `Authentication/SignIn` and `Authentication/RefreshToken` responses include `userID`
51
+ and `email` (see `ICurrentUserInfo` below).
52
+
53
+ - **`ExternalContentComponent` + `externalContentRoutes`** — the embedded view. Spread the routes
54
+ into `MainLayoutComponent`’s children:
55
+
56
+ ```ts
57
+ { path: '', component: MainLayoutComponent, canActivate: [AuthGuard], children: [
58
+ ...externalContentRoutes,
59
+ // your own features
60
+ ] }
61
+ ```
62
+
63
+ Register them even if you only plan to use `ExternalNewTab`: an embedded item configured without
64
+ them opens a tab that immediately bounces to the home route.
65
+
66
+ The view is an ordinary `TabViewBase` hosted by `DefaultTabViewComponent`, so its actions appear
67
+ in the application ribbon (a **Page** group with **Refresh** and **Open in a new browser tab**)
68
+ and look like every other screen’s — the shipped `externalContentRoutes` already wires that host
69
+ up, which is the other reason not to hand-write the routes. **Refresh** carries the same icon and
70
+ label as `framework-button-refresh`, because reloading a report is the same action as refreshing
71
+ a grid.
72
+
73
+ Refreshing genuinely tears the frame down and builds a new one — a cross-origin frame cannot be
74
+ navigated any other way — so the old render visibly goes away instead of sitting there while you
75
+ wonder whether anything happened. The button spins and the panel shows a loading overlay until
76
+ the destination reports `load`, or until the slow-frame delay elapses, so a destination that
77
+ never reports one cannot leave the controls stuck.
78
+
79
+ **Open in a new browser tab** is always available, because many sites refuse to be embedded
80
+ (`X-Frame-Options`, CSP `frame-ancestors`) and a browser gives JavaScript no reliable way to
81
+ detect that — if nothing has loaded after a few seconds the view also shows a hint saying so. An
82
+ `https` application cannot embed an `http` destination at all; the same button is the way out.
83
+
84
+ The tab URL is `/external-content/<menu id>` and never carries the destination, so no one can
85
+ hand-craft a link that makes your application frame an arbitrary site. Pressing F5 on an embedded
86
+ tab restores both the frame and the tab title from `sessionStorage`. Opening that URL in a *fresh*
87
+ browser tab can only work if your `SidebarService.getMenuFromUrl()` resolves
88
+ `/external-content/<id>`; otherwise the view says the content is unavailable and asks the user to
89
+ reopen it from the menu.
90
+
91
+ - **`EXTERNAL_CONTENT_CONFIGS`** — `allowedOrigins` (default `[]`, meaning any `http`/`https`
92
+ origin) and `slowFrameHintDelay` (default 5000 ms). **Populate `allowedOrigins` in production.**
93
+ Embedded and new-tab destinations are already rejected unless they are absolute `http`/`https`
94
+ URLs, which stops a `javascript:` or `data:` URL in your menu table from executing in your users’
95
+ session; an origin allowlist narrows what is left from “any site on the internet” to your known
96
+ report hosts. The iframe is sandboxed with a fixed, non-configurable token list that withholds
97
+ `allow-top-navigation`, so a framed site cannot navigate your application away. Do not point an
98
+ embedded item at your own application’s origin — use an internal route for that.
99
+
100
+ - **`ICurrentUserInfo.email`, `.userID`, `.username`** (all optional) — the values behind
101
+ `{email}`, `{userId}` and `{userName}`. `username` was already being persisted in the base64
102
+ `userInfo` entry and is now simply typed, so `{userName}` needs no backend change; `email` and
103
+ `userID` must be added to your sign-in and refresh responses.
104
+
105
+ - **Translations** for the embedded view under `i18n/external-content/`, already included in
106
+ `ZAMBON_SHARED_I18N_RESOURCES`.
107
+
108
+ ### Fixed
109
+
110
+ - **A deep link whose URL the menu API cannot resolve no longer surfaces an unhandled error.**
111
+ `MainLayoutComponent` resolves a deep-linked tab’s title through `SidebarService.getMenuFromUrl()`
112
+ and had no error handler, so a 404 became an unhandled rejection in the console. The title is
113
+ best-effort and the failure is now swallowed — which matters more now that the embedded-content
114
+ route is a URL shape most menu endpoints do not know.
115
+
116
+ ### ⚠ Breaking Changes / Migration
117
+
118
+ - **Requires `@zambon-dev/library` 1.4.0 or later.** `SidebarMenuOpenMode`,
119
+ `toSidebarMenuOpenMode` and `SidebarService.menuExternalUrlSelected` ship in that release. The
120
+ declared peer range still allows older versions, so upgrade both packages together.
121
+ - **To use external menu items, register `externalContentRoutes`** as children of
122
+ `MainLayoutComponent` (snippet above), and make your menu endpoint return `openMode`. The
123
+ resolver accepts it as the camelCase string (`"internal"`, `"externalNewTab"`,
124
+ `"externalEmbedded"`, matched case-insensitively) or as the enum ordinal (`0`, `1`, `2`), so a
125
+ plain ASP.NET Core enum property works with no converter.
126
+ - **The embedded view needs both i18n bundles registered.** Its own strings ship in this package
127
+ (`ZAMBON_SHARED_I18N_RESOURCES`, under `assets/i18n/zambon-dev/shared/external-content/`), but the
128
+ ribbon reuses three keys owned by `@zambon-dev/framework` — `RibbonGroup-Page`, `Button-Refresh`
129
+ and `Loading` — so `ZAMBON_FRAMEWORK_I18N_RESOURCES` has to be registered too, or those render as
130
+ raw keys. Any application that already uses framework buttons registers it; if your
131
+ `TranslateLoader` hand-lists prefixes instead of spreading the two constants, add both.
132
+ - Nothing else changes: applications with no `openMode` on any menu item behave exactly as before
133
+ and need no action.
134
+
135
+ ## [2.0.0] - 2026-07-30
136
+
137
+ ### Added
138
+
139
+ - **Storybook: `Shared/App Showcase` story** — a full-height, navigable demo of the complete
140
+ application shell, backed by in-memory mock data. Clicking the sidebar entries (Dashboard,
141
+ General ▸ Customers/Units, Security ▸ Users) opens tabs that render working list-views and
142
+ detail-views through the real hosts (`DefaultTabViewComponent` /
143
+ `DefaultDetailsTabViewComponent`, `TabViewList` / `FormView`, and the `framework-button-*`
144
+ ribbon buttons). It covers:
145
+ - a branded top bar (logo, app name, subtitle, environment badge and a working notifications
146
+ bell) and a versioned sidebar footer;
147
+ - a mocked audit/history view, reachable from the detail views' Views button;
148
+ - `framework-button-filters` (via a `FiltersBase` component per entity) and
149
+ `framework-button-export` on every list: the mock dataset honours `IListParameters.filters`, so
150
+ filtering visibly narrows the grid, and exporting downloads a real CSV of the filtered rows;
151
+ - a **child list of addresses** on the Customers detail view, edited through `lib-multi-editor` —
152
+ `ChildList` and `MultiEditorModal` together, with the accordion gated on `hasEntityID` (a child
153
+ collection needs a persisted parent), the Edit button opening the multi-editor rather than
154
+ entering form edit mode, and add/remove/edit applied as one batch. The Customers grid's City
155
+ column is **derived** from the customer's first address, so editing an address is reflected in
156
+ the parent list and its filter with nothing to keep in sync;
157
+ - mocked request latency, so loading states are observable;
158
+ - full `en`/`pt` translation, so the language selector switches the entire showcase.
159
+
160
+ Development-only: it lives entirely in `app-showcase.stories.ts`, which is excluded from the
161
+ package build.
162
+
163
+ ### Fixed
164
+
165
+ - **Audit history models now match the JSON the audit endpoints actually return.**
166
+ `IServicesHistoryList` and `IOperationsHistoryList` declared an `ID` property, and
167
+ `IOperationsHistoryList` declared `entityId`. Neither key is ever sent. The audit endpoints
168
+ (`POST /{controller}/{entityID}/Audit` and `.../Audit/{serviceHistoryID}`) serialize under
169
+ ASP.NET Core's default camelCase policy, which lowercases only a *leading* run of capitals — so the
170
+ backend's `ID` goes out as `id` and its `EntityID` goes out as `entityID`. Typing a row against
171
+ `.ID` or `.entityId` therefore compiled fine and read `undefined` at run time. See
172
+ **⚠ Breaking Changes / Migration** below.
173
+
174
+ This was also a latent trap for anything supplying its own audit rows: the grid resolves row
175
+ identity through `compareProperty`, which is `'id'`, so a row carrying only `ID` could never be
176
+ selected — clicking a service entry left the operations grid empty. Rows shaped like the real
177
+ payload work unchanged; `@shared`'s own runtime behaviour is not affected by this release.
178
+
179
+ - **The top bar's notifications bell is no longer pushed off-centre by its unread badge.** The badge
180
+ is an absolutely-positioned overlay, so it adds no width — but it is still a DOM sibling after the
181
+ bell icon, which defeated the `.btn i:not(:last-child)` margin guard in the global button styles.
182
+ That left 8px of dead space to the icon's right and widened the button from 38px to 46px, but only
183
+ while an unread count was showing, so it came and went with the count. The margin is now cleared
184
+ for that button specifically; the shared guard, which is correct for icon-plus-label buttons, is
185
+ unchanged.
186
+
187
+ - **Lint: `shared-` component/directive selector prefix is now accepted.**
188
+ `libs/shared/eslint.config.mjs` still carried the scaffolded `prefix: 'lib'`, so the library's own
189
+ `shared-`-prefixed components (`shared-main-layout`, `shared-login-layout`) failed
190
+ `@angular-eslint/component-selector`. Both selector rules now accept `['lib', 'shared']`.
191
+
192
+ ### ⚠ Breaking Changes / Migration
193
+
194
+ Two properties on the audit history models were renamed to the keys the backend actually sends. Both
195
+ models are exported from the package root, so anything typed against the old names will now fail to
196
+ compile. Nothing in `@shared` changes behaviour at run time — the fix is to the declared types.
197
+
198
+ | Model | Before | After |
199
+ |-------|--------|-------|
200
+ | `IServicesHistoryList` | `ID: number` | `id: number` |
201
+ | `IOperationsHistoryList` | `ID: number` | `id: number` |
202
+ | `IOperationsHistoryList` | `entityId?: number` | `entityID?: number` |
203
+
204
+ To upgrade:
205
+
206
+ 1. Rename `.ID` to `.id` wherever you read or construct an `IServicesHistoryList` or
207
+ `IOperationsHistoryList`. If the compiler now reports the property as missing, that code was
208
+ reading `undefined` before — it never matched the payload.
209
+ 2. Rename `.entityId` to `.entityID` on `IOperationsHistoryList`. The capitals are deliberate and
210
+ match the backend's `EntityID`; do not "correct" them back.
211
+ 3. If you supply audit rows yourself — a test double, a Storybook mock, a hand-rolled
212
+ `ServicesHistoryService` / `OperationsHistoryService` — emit `id`, not `ID`. Rows keyed only on
213
+ `ID` were never selectable, so a service row's selection could not reach the operations grid.
214
+ 4. If you worked around that by overriding `compareProperty` to `'ID'` in a `ServicesHistoryDataset`
215
+ or `OperationsHistoryDataset` subclass, remove the override. The inherited `'id'` is now correct.
216
+
26
217
  ## [1.2.0] - 2026-07-28
27
218
 
28
219
  ### Added
@@ -109,7 +300,9 @@ URL) in `AppConfig`, and implement a hub that pushes the notification list to cl
109
300
  available via [GitHub Releases](https://github.com/RicardoZambon/ZLibraries/releases) and the
110
301
  `shared-v*` tags.
111
302
 
112
- [Unreleased]: https://github.com/RicardoZambon/ZLibraries/compare/shared-v1.2.0...HEAD
303
+ [Unreleased]: https://github.com/RicardoZambon/ZLibraries/compare/shared-v2.1.0...HEAD
304
+ [2.1.0]: https://github.com/RicardoZambon/ZLibraries/releases/tag/shared-v2.1.0
305
+ [2.0.0]: https://github.com/RicardoZambon/ZLibraries/releases/tag/shared-v2.0.0
113
306
  [1.2.0]: https://github.com/RicardoZambon/ZLibraries/releases/tag/shared-v1.2.0
114
307
  [1.1.0]: https://github.com/RicardoZambon/ZLibraries/releases/tag/shared-v1.1.0
115
308
  [1.0.2]: https://github.com/RicardoZambon/ZLibraries/releases/tag/shared-v1.0.2