@ardimedia/angular-portal-azure 0.3.25 → 0.3.28
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/README.md
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
# AngularPortalAzure
|
|
2
|
-
|
|
3
|
-
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.1.0.
|
|
4
|
-
|
|
5
|
-
## Code scaffolding
|
|
6
|
-
|
|
7
|
-
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
ng generate component component-name
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
ng generate --help
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Building
|
|
20
|
-
|
|
21
|
-
To build the library, run:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
ng build angular-portal-azure
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
|
|
28
|
-
|
|
29
|
-
### Publishing the Library
|
|
30
|
-
|
|
31
|
-
Once the project is built, you can publish your library by following these steps:
|
|
32
|
-
|
|
33
|
-
1. Navigate to the `dist` directory:
|
|
34
|
-
```bash
|
|
35
|
-
cd dist/angular-portal-azure
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
2. Run the `npm publish` command to publish your library to the npm registry:
|
|
39
|
-
```bash
|
|
40
|
-
npm publish
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Running unit tests
|
|
44
|
-
|
|
45
|
-
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
ng test
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Running end-to-end tests
|
|
52
|
-
|
|
53
|
-
For end-to-end (e2e) testing, run:
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
ng e2e
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
|
60
|
-
|
|
61
|
-
## Additional Resources
|
|
62
|
-
|
|
63
|
-
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
|
1
|
+
# AngularPortalAzure
|
|
2
|
+
|
|
3
|
+
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.1.0.
|
|
4
|
+
|
|
5
|
+
## Code scaffolding
|
|
6
|
+
|
|
7
|
+
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
ng generate component component-name
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
ng generate --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Building
|
|
20
|
+
|
|
21
|
+
To build the library, run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
ng build angular-portal-azure
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
|
|
28
|
+
|
|
29
|
+
### Publishing the Library
|
|
30
|
+
|
|
31
|
+
Once the project is built, you can publish your library by following these steps:
|
|
32
|
+
|
|
33
|
+
1. Navigate to the `dist` directory:
|
|
34
|
+
```bash
|
|
35
|
+
cd dist/angular-portal-azure
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
2. Run the `npm publish` command to publish your library to the npm registry:
|
|
39
|
+
```bash
|
|
40
|
+
npm publish
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Running unit tests
|
|
44
|
+
|
|
45
|
+
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
ng test
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Running end-to-end tests
|
|
52
|
+
|
|
53
|
+
For end-to-end (e2e) testing, run:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
ng e2e
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
|
60
|
+
|
|
61
|
+
## Additional Resources
|
|
62
|
+
|
|
63
|
+
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, computed, Injectable, inject,
|
|
2
|
+
import { signal, computed, Injectable, inject, InjectionToken, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER, DestroyRef, effect, APP_INITIALIZER, input, output, Component, ElementRef, Injector, afterNextRender, contentChild } from '@angular/core';
|
|
3
3
|
import { Router, NavigationEnd } from '@angular/router';
|
|
4
4
|
import { filter } from 'rxjs/operators';
|
|
5
5
|
import { DOCUMENT, NgComponentOutlet } from '@angular/common';
|
|
6
|
+
import { NgForm } from '@angular/forms';
|
|
6
7
|
|
|
7
8
|
function clearStatusBar() {
|
|
8
9
|
return { text: '', style: 'none' };
|
|
@@ -72,6 +73,7 @@ const LABELS_DE_CH = {
|
|
|
72
73
|
settings: 'Einstellungen',
|
|
73
74
|
language: 'Sprache',
|
|
74
75
|
appearance: 'Darstellung',
|
|
76
|
+
unsavedChangesConfirm: 'Die Änderungen wurden noch nicht gespeichert. Trotzdem verlassen?',
|
|
75
77
|
};
|
|
76
78
|
/** German (Germany) — Swiss spelling rules apply (no ß) */
|
|
77
79
|
const LABELS_DE_DE = { ...LABELS_DE_CH };
|
|
@@ -101,6 +103,7 @@ const LABELS_EN = {
|
|
|
101
103
|
settings: 'Settings',
|
|
102
104
|
language: 'Language',
|
|
103
105
|
appearance: 'Appearance',
|
|
106
|
+
unsavedChangesConfirm: 'You have unsaved changes. Leave anyway?',
|
|
104
107
|
};
|
|
105
108
|
/** French */
|
|
106
109
|
const LABELS_FR = {
|
|
@@ -128,6 +131,7 @@ const LABELS_FR = {
|
|
|
128
131
|
settings: 'Paramètres',
|
|
129
132
|
language: 'Langue',
|
|
130
133
|
appearance: 'Apparence',
|
|
134
|
+
unsavedChangesConfirm: 'Les modifications n\'ont pas été enregistrées. Quitter quand même ?',
|
|
131
135
|
};
|
|
132
136
|
/** Spanish */
|
|
133
137
|
const LABELS_ES = {
|
|
@@ -155,6 +159,7 @@ const LABELS_ES = {
|
|
|
155
159
|
settings: 'Configuración',
|
|
156
160
|
language: 'Idioma',
|
|
157
161
|
appearance: 'Apariencia',
|
|
162
|
+
unsavedChangesConfirm: 'Hay cambios sin guardar. ¿Salir de todos modos?',
|
|
158
163
|
};
|
|
159
164
|
/** Italian */
|
|
160
165
|
const LABELS_IT = {
|
|
@@ -182,6 +187,7 @@ const LABELS_IT = {
|
|
|
182
187
|
settings: 'Impostazioni',
|
|
183
188
|
language: 'Lingua',
|
|
184
189
|
appearance: 'Aspetto',
|
|
190
|
+
unsavedChangesConfirm: 'Le modifiche non sono state salvate. Uscire comunque?',
|
|
185
191
|
};
|
|
186
192
|
// ── Language preset registry ────────────────────────────────────────
|
|
187
193
|
/** Keep DEFAULT_LABELS as alias for backward compatibility */
|
|
@@ -689,6 +695,70 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImpor
|
|
|
689
695
|
class BladeService {
|
|
690
696
|
portal = inject(PortalService);
|
|
691
697
|
registry = inject(BladeRegistry);
|
|
698
|
+
/**
|
|
699
|
+
* Unsaved-changes guard registry: blade path -> predicate returning true when the blade has
|
|
700
|
+
* unsaved edits. Populated by apa-blade-detail (from the projected NgForm). Consulted before a
|
|
701
|
+
* blade is closed/replaced or the page is left, so the user can confirm discarding changes.
|
|
702
|
+
* Message can be localised via PortalLabels.unsavedChangesConfirm; the navigation flow is
|
|
703
|
+
* synchronous, so a synchronous window.confirm is used.
|
|
704
|
+
*/
|
|
705
|
+
dirtyChecks = new Map();
|
|
706
|
+
/** Optional override for the confirmation message (defaults to the de-CH label). */
|
|
707
|
+
unsavedChangesMessage = DEFAULT_LABELS.unsavedChangesConfirm;
|
|
708
|
+
constructor() {
|
|
709
|
+
// Native browser guard when the whole page/tab is closed or reloaded with unsaved edits.
|
|
710
|
+
if (typeof window !== 'undefined') {
|
|
711
|
+
window.addEventListener('beforeunload', (e) => {
|
|
712
|
+
if (this.anyDirty()) {
|
|
713
|
+
e.preventDefault();
|
|
714
|
+
e.returnValue = '';
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/** Register a blade's unsaved-changes predicate (called by apa-blade-detail). */
|
|
720
|
+
registerDirtyCheck(path, isDirty) {
|
|
721
|
+
this.dirtyChecks.set(path.toLowerCase(), isDirty);
|
|
722
|
+
}
|
|
723
|
+
/** Remove a blade's unsaved-changes predicate (called when the detail is destroyed). */
|
|
724
|
+
unregisterDirtyCheck(path) {
|
|
725
|
+
this.dirtyChecks.delete(path.toLowerCase());
|
|
726
|
+
}
|
|
727
|
+
isPathDirty(path) {
|
|
728
|
+
const check = this.dirtyChecks.get(path.toLowerCase());
|
|
729
|
+
try {
|
|
730
|
+
return check ? check() === true : false;
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
anyDirty() {
|
|
737
|
+
for (const path of this.dirtyChecks.keys()) {
|
|
738
|
+
if (this.isPathDirty(path))
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Returns true if it's OK to proceed with removing the given blade paths: none of them is dirty,
|
|
745
|
+
* or the user confirmed discarding the changes. Shows one confirmation regardless of how many
|
|
746
|
+
* dirty blades are affected.
|
|
747
|
+
*/
|
|
748
|
+
mayDiscard(removedPaths) {
|
|
749
|
+
const anyDirty = removedPaths.some((p) => this.isPathDirty(p));
|
|
750
|
+
if (!anyDirty)
|
|
751
|
+
return true;
|
|
752
|
+
return typeof window === 'undefined' ? true : window.confirm(this.unsavedChangesMessage);
|
|
753
|
+
}
|
|
754
|
+
/** Drop dirty-check entries for paths that are no longer open. */
|
|
755
|
+
pruneDirtyChecks() {
|
|
756
|
+
const open = new Set(this.portal.blades().map((b) => b.path));
|
|
757
|
+
for (const path of [...this.dirtyChecks.keys()]) {
|
|
758
|
+
if (!open.has(path))
|
|
759
|
+
this.dirtyChecks.delete(path);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
692
762
|
/**
|
|
693
763
|
* Set the first blade (e.g., when opening a top-level item from a tile).
|
|
694
764
|
* Clears all existing blades, hides panorama, and adds the new blade.
|
|
@@ -696,6 +766,11 @@ class BladeService {
|
|
|
696
766
|
* Ported from AreaBlades.setFirstBlade() in v0.2.346.
|
|
697
767
|
*/
|
|
698
768
|
setFirstBlade(path, title = '', width) {
|
|
769
|
+
const existing = this.portal.blades();
|
|
770
|
+
if (existing.length > 0 && !this.mayDiscard(existing.map((b) => b.path))) {
|
|
771
|
+
return existing[0];
|
|
772
|
+
}
|
|
773
|
+
this.dirtyChecks.clear();
|
|
699
774
|
this.portal.blades.set([]);
|
|
700
775
|
const entry = this.registry.getEntry(path);
|
|
701
776
|
const blade = createBlade(path.toLowerCase(), title || entry?.title || path, width ?? entry?.width ?? 315);
|
|
@@ -715,7 +790,8 @@ class BladeService {
|
|
|
715
790
|
// Cascade close first: remove blades after the sender
|
|
716
791
|
// This ensures a blade at the same path gets recreated with new params
|
|
717
792
|
if (senderPath) {
|
|
718
|
-
this.clearChild(senderPath)
|
|
793
|
+
if (!this.clearChild(senderPath))
|
|
794
|
+
return undefined;
|
|
719
795
|
}
|
|
720
796
|
// Check if blade already exists (after cascade close)
|
|
721
797
|
const existing = this.portal.blades().find((b) => b.path === normalizedPath);
|
|
@@ -739,7 +815,10 @@ class BladeService {
|
|
|
739
815
|
* Ported from AreaBlades.clearAll() in v0.2.346.
|
|
740
816
|
*/
|
|
741
817
|
clearAll() {
|
|
818
|
+
if (!this.mayDiscard(this.portal.blades().map((b) => b.path)))
|
|
819
|
+
return;
|
|
742
820
|
this.portal.blades.set([]);
|
|
821
|
+
this.dirtyChecks.clear();
|
|
743
822
|
}
|
|
744
823
|
/**
|
|
745
824
|
* Remove a specific blade and all blades to its right.
|
|
@@ -752,7 +831,10 @@ class BladeService {
|
|
|
752
831
|
const blades = this.portal.blades();
|
|
753
832
|
const index = blades.findIndex((b) => b.path === normalizedPath);
|
|
754
833
|
if (index >= 0) {
|
|
834
|
+
if (!this.mayDiscard(blades.slice(index).map((b) => b.path)))
|
|
835
|
+
return false;
|
|
755
836
|
this.portal.blades.set(blades.slice(0, index));
|
|
837
|
+
this.pruneDirtyChecks();
|
|
756
838
|
}
|
|
757
839
|
else {
|
|
758
840
|
// Check notification area
|
|
@@ -761,6 +843,7 @@ class BladeService {
|
|
|
761
843
|
this.portal.hideNotification();
|
|
762
844
|
}
|
|
763
845
|
}
|
|
846
|
+
return true;
|
|
764
847
|
}
|
|
765
848
|
/**
|
|
766
849
|
* Remove all blades AFTER a given path (keeps the blade itself).
|
|
@@ -770,13 +853,17 @@ class BladeService {
|
|
|
770
853
|
*/
|
|
771
854
|
clearChild(path) {
|
|
772
855
|
if (!path)
|
|
773
|
-
return;
|
|
856
|
+
return true;
|
|
774
857
|
const normalizedPath = path.toLowerCase();
|
|
775
858
|
const blades = this.portal.blades();
|
|
776
859
|
const index = blades.findIndex((b) => b.path === normalizedPath);
|
|
777
860
|
if (index >= 0) {
|
|
861
|
+
if (!this.mayDiscard(blades.slice(index + 1).map((b) => b.path)))
|
|
862
|
+
return false;
|
|
778
863
|
this.portal.blades.set(blades.slice(0, index + 1));
|
|
864
|
+
this.pruneDirtyChecks();
|
|
779
865
|
}
|
|
866
|
+
return true;
|
|
780
867
|
}
|
|
781
868
|
/**
|
|
782
869
|
* Remove blades at and beyond a specific 1-based level.
|
|
@@ -786,7 +873,10 @@ class BladeService {
|
|
|
786
873
|
const adjustedLevel = level <= 0 ? 1 : level;
|
|
787
874
|
const blades = this.portal.blades();
|
|
788
875
|
if (adjustedLevel <= blades.length) {
|
|
876
|
+
if (!this.mayDiscard(blades.slice(adjustedLevel - 1).map((b) => b.path)))
|
|
877
|
+
return;
|
|
789
878
|
this.portal.blades.set(blades.slice(0, adjustedLevel - 1));
|
|
879
|
+
this.pruneDirtyChecks();
|
|
790
880
|
}
|
|
791
881
|
}
|
|
792
882
|
/**
|
|
@@ -796,7 +886,10 @@ class BladeService {
|
|
|
796
886
|
clearLastLevel() {
|
|
797
887
|
const blades = this.portal.blades();
|
|
798
888
|
if (blades.length > 0) {
|
|
889
|
+
if (!this.mayDiscard([blades[blades.length - 1].path]))
|
|
890
|
+
return;
|
|
799
891
|
this.portal.blades.set(blades.slice(0, -1));
|
|
892
|
+
this.pruneDirtyChecks();
|
|
800
893
|
}
|
|
801
894
|
}
|
|
802
895
|
/**
|
|
@@ -831,7 +924,43 @@ class BladeService {
|
|
|
831
924
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeService, decorators: [{
|
|
832
925
|
type: Injectable,
|
|
833
926
|
args: [{ providedIn: 'root' }]
|
|
834
|
-
}] });
|
|
927
|
+
}], ctorParameters: () => [] });
|
|
928
|
+
|
|
929
|
+
/** @internal */
|
|
930
|
+
const BLADE_ROUTER_CONFIG = new InjectionToken('BLADE_ROUTER_CONFIG');
|
|
931
|
+
/**
|
|
932
|
+
* Enables opt-in URL synchronization for the blade stack.
|
|
933
|
+
*
|
|
934
|
+
* Add alongside `provideRouter()` and `providePortalAzure()`:
|
|
935
|
+
* ```typescript
|
|
936
|
+
* export const appConfig: ApplicationConfig = {
|
|
937
|
+
* providers: [
|
|
938
|
+
* provideRouter(routes),
|
|
939
|
+
* providePortalAzure({ title: 'My Portal', ... }),
|
|
940
|
+
* provideBladeRouter(),
|
|
941
|
+
* ],
|
|
942
|
+
* };
|
|
943
|
+
* ```
|
|
944
|
+
*
|
|
945
|
+
* Optionally pass a config to set a fixed route prefix:
|
|
946
|
+
* ```typescript
|
|
947
|
+
* provideBladeRouter({ prefix: 'app' }) // → /app/customers/list
|
|
948
|
+
* provideBladeRouter({ prefix: '' }) // → /customers/list (no prefix)
|
|
949
|
+
* ```
|
|
950
|
+
*
|
|
951
|
+
* Without this provider, blade navigation remains purely in-memory.
|
|
952
|
+
*/
|
|
953
|
+
function provideBladeRouter(config) {
|
|
954
|
+
return makeEnvironmentProviders([
|
|
955
|
+
BladeRouterService,
|
|
956
|
+
{ provide: BLADE_ROUTER_CONFIG, useValue: config ?? {} },
|
|
957
|
+
{
|
|
958
|
+
provide: ENVIRONMENT_INITIALIZER,
|
|
959
|
+
multi: true,
|
|
960
|
+
useFactory: () => () => inject(BladeRouterService),
|
|
961
|
+
},
|
|
962
|
+
]);
|
|
963
|
+
}
|
|
835
964
|
|
|
836
965
|
/**
|
|
837
966
|
* Optional service that syncs the blade stack with the browser URL.
|
|
@@ -849,6 +978,7 @@ class BladeRouterService {
|
|
|
849
978
|
portal = inject(PortalService);
|
|
850
979
|
registry = inject(BladeRegistry);
|
|
851
980
|
destroyRef = inject(DestroyRef);
|
|
981
|
+
config = inject(BLADE_ROUTER_CONFIG, { optional: true }) ?? {};
|
|
852
982
|
_syncingFromUrl = false;
|
|
853
983
|
_initialRestoreDone = false;
|
|
854
984
|
constructor() {
|
|
@@ -859,9 +989,11 @@ class BladeRouterService {
|
|
|
859
989
|
return;
|
|
860
990
|
if (blades.length === 0 && !this._initialRestoreDone)
|
|
861
991
|
return;
|
|
862
|
-
const
|
|
992
|
+
const prefix = this.getEffectivePrefix();
|
|
863
993
|
const bladePath = this.encodeBladesToPath(blades);
|
|
864
|
-
const targetUrl =
|
|
994
|
+
const targetUrl = prefix
|
|
995
|
+
? (bladePath ? `/${prefix}/${bladePath}` : `/${prefix}`)
|
|
996
|
+
: (bladePath ? `/${bladePath}` : `/`);
|
|
865
997
|
const currentPath = this.router.url.split('?')[0].split(';')[0];
|
|
866
998
|
// Only navigate if the path actually changed (avoid loops)
|
|
867
999
|
if (this.normalizeUrl(currentPath) !== this.normalizeUrl(targetUrl)) {
|
|
@@ -986,12 +1118,19 @@ class BladeRouterService {
|
|
|
986
1118
|
}
|
|
987
1119
|
/** Restore blade stack from a path-based URL */
|
|
988
1120
|
restoreFromPath(url) {
|
|
989
|
-
const
|
|
1121
|
+
const prefix = this.getEffectivePrefix();
|
|
990
1122
|
const path = url.split('?')[0]; // strip query params
|
|
991
|
-
|
|
992
|
-
if (
|
|
993
|
-
|
|
994
|
-
|
|
1123
|
+
let pathAfterPrefix;
|
|
1124
|
+
if (prefix) {
|
|
1125
|
+
const prefixPattern = '/' + prefix;
|
|
1126
|
+
if (!path.startsWith(prefixPattern))
|
|
1127
|
+
return;
|
|
1128
|
+
pathAfterPrefix = path.substring(prefixPattern.length + 1); // +1 for trailing /
|
|
1129
|
+
}
|
|
1130
|
+
else {
|
|
1131
|
+
// No prefix: everything after the leading / is blade path
|
|
1132
|
+
pathAfterPrefix = path.substring(1);
|
|
1133
|
+
}
|
|
995
1134
|
const newBlades = this.decodeBladesFromPath(pathAfterPrefix);
|
|
996
1135
|
const currentPaths = this.portal.blades().map((b) => b.path);
|
|
997
1136
|
const newPaths = newBlades.map((b) => b.path);
|
|
@@ -1054,12 +1193,25 @@ class BladeRouterService {
|
|
|
1054
1193
|
const entry = this.registry.getEntry(path);
|
|
1055
1194
|
return createBlade(path, entry?.title ?? path, entry?.width ?? 315);
|
|
1056
1195
|
});
|
|
1057
|
-
const
|
|
1196
|
+
const prefix = this.getEffectivePrefix();
|
|
1058
1197
|
const bladePath = this.encodeBladesToPath(blades);
|
|
1059
|
-
const newUrl =
|
|
1198
|
+
const newUrl = prefix
|
|
1199
|
+
? (bladePath ? `/${prefix}/${bladePath}` : `/${prefix}`)
|
|
1200
|
+
: (bladePath ? `/${bladePath}` : `/`);
|
|
1060
1201
|
// Redirect to new format
|
|
1061
1202
|
this.router.navigateByUrl(newUrl, { replaceUrl: true });
|
|
1062
1203
|
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Return the effective route prefix. If a prefix was configured via
|
|
1206
|
+
* `provideBladeRouter({ prefix })`, use it (including empty string).
|
|
1207
|
+
* Otherwise fall back to dynamically reading the first URL segment.
|
|
1208
|
+
*/
|
|
1209
|
+
getEffectivePrefix() {
|
|
1210
|
+
if (this.config.prefix !== undefined) {
|
|
1211
|
+
return this.config.prefix;
|
|
1212
|
+
}
|
|
1213
|
+
return this.getRoutePrefix();
|
|
1214
|
+
}
|
|
1063
1215
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeRouterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1064
1216
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeRouterService });
|
|
1065
1217
|
}
|
|
@@ -1098,36 +1250,6 @@ function providePortalAzure(config) {
|
|
|
1098
1250
|
]);
|
|
1099
1251
|
}
|
|
1100
1252
|
|
|
1101
|
-
/**
|
|
1102
|
-
* Enables opt-in URL synchronization for the blade stack.
|
|
1103
|
-
*
|
|
1104
|
-
* Add alongside `provideRouter()` and `providePortalAzure()`:
|
|
1105
|
-
* ```typescript
|
|
1106
|
-
* export const appConfig: ApplicationConfig = {
|
|
1107
|
-
* providers: [
|
|
1108
|
-
* provideRouter(routes),
|
|
1109
|
-
* providePortalAzure({ title: 'My Portal', ... }),
|
|
1110
|
-
* provideBladeRouter(),
|
|
1111
|
-
* ],
|
|
1112
|
-
* };
|
|
1113
|
-
* ```
|
|
1114
|
-
*
|
|
1115
|
-
* When enabled, blade paths sync to the URL as a query parameter:
|
|
1116
|
-
* `?blades=customers,customers/list,customers/1`
|
|
1117
|
-
*
|
|
1118
|
-
* Without this provider, blade navigation remains purely in-memory.
|
|
1119
|
-
*/
|
|
1120
|
-
function provideBladeRouter() {
|
|
1121
|
-
return makeEnvironmentProviders([
|
|
1122
|
-
BladeRouterService,
|
|
1123
|
-
{
|
|
1124
|
-
provide: ENVIRONMENT_INITIALIZER,
|
|
1125
|
-
multi: true,
|
|
1126
|
-
useFactory: () => () => inject(BladeRouterService),
|
|
1127
|
-
},
|
|
1128
|
-
]);
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
1253
|
/**
|
|
1132
1254
|
* Individual dashboard tile.
|
|
1133
1255
|
* Ported from the tile section in home.html (v0.2.346).
|
|
@@ -1322,6 +1444,17 @@ class PortalLayoutComponent {
|
|
|
1322
1444
|
isDark = signal(false, ...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
|
|
1323
1445
|
/** Available languages from the preset registry */
|
|
1324
1446
|
availableLanguages = Array.from(LANGUAGE_PRESETS.values()).map((p) => ({ code: p.code, displayName: p.displayName }));
|
|
1447
|
+
/**
|
|
1448
|
+
* Handle an avatar dropdown item click. If the item defines an action (e.g. open a blade), run it
|
|
1449
|
+
* and suppress the default link navigation; otherwise let the href navigate normally.
|
|
1450
|
+
*/
|
|
1451
|
+
onAvatarItemClick(item, event) {
|
|
1452
|
+
this.portal.closeAvatarMenu();
|
|
1453
|
+
if (item.action) {
|
|
1454
|
+
event.preventDefault();
|
|
1455
|
+
item.action();
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1325
1458
|
constructor() {
|
|
1326
1459
|
const stored = localStorage.getItem(PortalLayoutComponent.STORAGE_KEY);
|
|
1327
1460
|
const dark = stored === 'true';
|
|
@@ -1499,8 +1632,9 @@ class PortalLayoutComponent {
|
|
|
1499
1632
|
</a>
|
|
1500
1633
|
@if (portal.avatarMenu().isOpen && portal.avatarMenu().items.length > 0) {
|
|
1501
1634
|
<div class="apa-avatar-dropdown" role="menu">
|
|
1502
|
-
@for (item of portal.avatarMenu().items; track item.
|
|
1503
|
-
<a class="apa-avatar-dropdown-item" role="menuitem" [href]="item.href"
|
|
1635
|
+
@for (item of portal.avatarMenu().items; track item.label) {
|
|
1636
|
+
<a class="apa-avatar-dropdown-item" role="menuitem" [href]="item.href"
|
|
1637
|
+
(click)="onAvatarItemClick(item, $event)">
|
|
1504
1638
|
@if (item.icon) {
|
|
1505
1639
|
<i [class]="item.icon" aria-hidden="true"></i>
|
|
1506
1640
|
}
|
|
@@ -1591,8 +1725,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImpor
|
|
|
1591
1725
|
</a>
|
|
1592
1726
|
@if (portal.avatarMenu().isOpen && portal.avatarMenu().items.length > 0) {
|
|
1593
1727
|
<div class="apa-avatar-dropdown" role="menu">
|
|
1594
|
-
@for (item of portal.avatarMenu().items; track item.
|
|
1595
|
-
<a class="apa-avatar-dropdown-item" role="menuitem" [href]="item.href"
|
|
1728
|
+
@for (item of portal.avatarMenu().items; track item.label) {
|
|
1729
|
+
<a class="apa-avatar-dropdown-item" role="menuitem" [href]="item.href"
|
|
1730
|
+
(click)="onAvatarItemClick(item, $event)">
|
|
1596
1731
|
@if (item.icon) {
|
|
1597
1732
|
<i [class]="item.icon" aria-hidden="true"></i>
|
|
1598
1733
|
}
|
|
@@ -1857,28 +1992,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImpor
|
|
|
1857
1992
|
*
|
|
1858
1993
|
* Usage:
|
|
1859
1994
|
* ```html
|
|
1995
|
+
* <!-- Default: each component is wrapped in <apa-blade> -->
|
|
1860
1996
|
* <apa-blade-host />
|
|
1997
|
+
*
|
|
1998
|
+
* <!-- No wrapper: components render directly (they manage their own blade chrome) -->
|
|
1999
|
+
* <apa-blade-host [wrapBlade]="false" />
|
|
1861
2000
|
* ```
|
|
1862
2001
|
*/
|
|
1863
2002
|
class BladeHostComponent {
|
|
2003
|
+
/** Whether to wrap each component in an `<apa-blade>` element. Default: true. */
|
|
2004
|
+
wrapBlade = input(true, ...(ngDevMode ? [{ debugName: "wrapBlade" }] : /* istanbul ignore next */ []));
|
|
1864
2005
|
portal = inject(PortalService);
|
|
1865
2006
|
registry = inject(BladeRegistry);
|
|
1866
2007
|
getComponent(path) {
|
|
1867
2008
|
return this.registry.get(path) ?? null;
|
|
1868
2009
|
}
|
|
1869
2010
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1870
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.3", type: BladeHostComponent, isStandalone: true, selector: "apa-blade-host", ngImport: i0, template: `
|
|
2011
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.3", type: BladeHostComponent, isStandalone: true, selector: "apa-blade-host", inputs: { wrapBlade: { classPropertyName: "wrapBlade", publicName: "wrapBlade", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
1871
2012
|
<div id="apa-blade-area" class="fxs-journey-target fxs-journey">
|
|
1872
2013
|
<div class="fxs-journey-layout fxs-stacklayout fxs-stacklayout-horizontal">
|
|
1873
2014
|
@for (blade of portal.blades(); track blade.uid) {
|
|
1874
2015
|
<div class="azureportalblade fxs-stacklayout-child">
|
|
1875
|
-
|
|
2016
|
+
@if (wrapBlade()) {
|
|
2017
|
+
<apa-blade [blade]="blade">
|
|
2018
|
+
@if (getComponent(blade.path); as component) {
|
|
2019
|
+
<ng-container *ngComponentOutlet="component" />
|
|
2020
|
+
} @else {
|
|
2021
|
+
<p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
|
|
2022
|
+
}
|
|
2023
|
+
</apa-blade>
|
|
2024
|
+
} @else {
|
|
1876
2025
|
@if (getComponent(blade.path); as component) {
|
|
1877
2026
|
<ng-container *ngComponentOutlet="component" />
|
|
1878
2027
|
} @else {
|
|
1879
2028
|
<p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
|
|
1880
2029
|
}
|
|
1881
|
-
|
|
2030
|
+
}
|
|
1882
2031
|
</div>
|
|
1883
2032
|
}
|
|
1884
2033
|
</div>
|
|
@@ -1892,19 +2041,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImpor
|
|
|
1892
2041
|
<div class="fxs-journey-layout fxs-stacklayout fxs-stacklayout-horizontal">
|
|
1893
2042
|
@for (blade of portal.blades(); track blade.uid) {
|
|
1894
2043
|
<div class="azureportalblade fxs-stacklayout-child">
|
|
1895
|
-
|
|
2044
|
+
@if (wrapBlade()) {
|
|
2045
|
+
<apa-blade [blade]="blade">
|
|
2046
|
+
@if (getComponent(blade.path); as component) {
|
|
2047
|
+
<ng-container *ngComponentOutlet="component" />
|
|
2048
|
+
} @else {
|
|
2049
|
+
<p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
|
|
2050
|
+
}
|
|
2051
|
+
</apa-blade>
|
|
2052
|
+
} @else {
|
|
1896
2053
|
@if (getComponent(blade.path); as component) {
|
|
1897
2054
|
<ng-container *ngComponentOutlet="component" />
|
|
1898
2055
|
} @else {
|
|
1899
2056
|
<p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
|
|
1900
2057
|
}
|
|
1901
|
-
|
|
2058
|
+
}
|
|
1902
2059
|
</div>
|
|
1903
2060
|
}
|
|
1904
2061
|
</div>
|
|
1905
2062
|
</div>
|
|
1906
2063
|
`, styles: [":host{display:block;height:100%}\n"] }]
|
|
1907
|
-
}] });
|
|
2064
|
+
}], propDecorators: { wrapBlade: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrapBlade", required: false }] }] } });
|
|
1908
2065
|
|
|
1909
2066
|
/**
|
|
1910
2067
|
* Navigation blade content — renders a list of nav items.
|
|
@@ -2145,8 +2302,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImpor
|
|
|
2145
2302
|
*/
|
|
2146
2303
|
class BladeDetailComponent {
|
|
2147
2304
|
blade = input.required(...(ngDevMode ? [{ debugName: "blade" }] : /* istanbul ignore next */ []));
|
|
2305
|
+
bladeService = inject(BladeService);
|
|
2306
|
+
/** The template-driven form projected into this detail, if any (auto-discovered). */
|
|
2307
|
+
form = contentChild(NgForm, { ...(ngDevMode ? { debugName: "form" } : /* istanbul ignore next */ {}), descendants: true });
|
|
2308
|
+
ngAfterContentInit() {
|
|
2309
|
+
const blade = this.blade();
|
|
2310
|
+
const isDirty = () => this.form()?.dirty ?? false;
|
|
2311
|
+
// Expose on the lifecycle (for consumers) and register with the BladeService so navigation
|
|
2312
|
+
// away from a dirty blade prompts for confirmation. No per-detail wiring needed — every
|
|
2313
|
+
// detail that projects a <form> is guarded automatically.
|
|
2314
|
+
blade.lifecycle.isDirty = isDirty;
|
|
2315
|
+
this.bladeService.registerDirtyCheck(blade.path, isDirty);
|
|
2316
|
+
// After a successful save, reset the form to pristine so the guard does not fire on a
|
|
2317
|
+
// saved-but-not-yet-navigated blade. Wrap (don't replace) any existing onSavedItem hook.
|
|
2318
|
+
const originalOnSaved = blade.lifecycle.onSavedItem;
|
|
2319
|
+
blade.lifecycle.onSavedItem = () => {
|
|
2320
|
+
this.form()?.form.markAsPristine();
|
|
2321
|
+
originalOnSaved?.();
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
ngOnDestroy() {
|
|
2325
|
+
this.bladeService.unregisterDirtyCheck(this.blade().path);
|
|
2326
|
+
}
|
|
2148
2327
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.3", ngImport: i0, type: BladeDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2149
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.
|
|
2328
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.3", type: BladeDetailComponent, isStandalone: true, selector: "apa-blade-detail", inputs: { blade: { classPropertyName: "blade", publicName: "blade", isSignal: true, isRequired: true, transformFunction: null } }, queries: [{ propertyName: "form", first: true, predicate: NgForm, descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
2150
2329
|
<div class="apa-blade-detail">
|
|
2151
2330
|
<ng-content />
|
|
2152
2331
|
</div>
|
|
@@ -2159,7 +2338,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImpor
|
|
|
2159
2338
|
<ng-content />
|
|
2160
2339
|
</div>
|
|
2161
2340
|
`, styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0}.apa-blade-detail{flex:1;display:flex;flex-direction:column;min-height:0}\n"] }]
|
|
2162
|
-
}], propDecorators: { blade: [{ type: i0.Input, args: [{ isSignal: true, alias: "blade", required: true }] }] } });
|
|
2341
|
+
}], propDecorators: { blade: [{ type: i0.Input, args: [{ isSignal: true, alias: "blade", required: true }] }], form: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NgForm), { ...{ descendants: true }, isSignal: true }] }] } });
|
|
2163
2342
|
/**
|
|
2164
2343
|
* Create standard detail blade commands (new, save, delete, cancel).
|
|
2165
2344
|
* Convenience function for setting up typical detail/edit blade commands
|
|
@@ -2410,5 +2589,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImpor
|
|
|
2410
2589
|
* Generated bundle index. Do not edit.
|
|
2411
2590
|
*/
|
|
2412
2591
|
|
|
2413
|
-
export { AvatarMenuComponent, BladeComponent, BladeDetailComponent, BladeGridComponent, BladeHostComponent, BladeNavComponent, BladeRegistry, BladeRouterService, BladeService, CommandBarComponent, DEFAULT_LABELS, LABELS_DE_CH, LABELS_DE_DE, LABELS_EN, LABELS_ES, LABELS_FR, LABELS_IT, LANGUAGE_PRESETS, NotificationPanelComponent, PanoramaComponent, PortalLayoutComponent, PortalService, SidebarComponent, TILE_DIMENSIONS, TileComponent, TileSize, clearStatusBar, createAvatarMenu, createBlade, createCommand, createDataBlade, createDetailCommands, createNavItem, createNotificationPanel, createPanorama, createTile, executeDeleteItem, executeLoadItem, executeLoadItems, executeSaveItem, filterItems, getUserDisplayName, layoutTiles, nextBladeUid, provideBladeRouter, providePortalAzure, registerLanguagePreset, statusBarError, statusBarInfo, statusBarSuccess };
|
|
2592
|
+
export { AvatarMenuComponent, BLADE_ROUTER_CONFIG, BladeComponent, BladeDetailComponent, BladeGridComponent, BladeHostComponent, BladeNavComponent, BladeRegistry, BladeRouterService, BladeService, CommandBarComponent, DEFAULT_LABELS, LABELS_DE_CH, LABELS_DE_DE, LABELS_EN, LABELS_ES, LABELS_FR, LABELS_IT, LANGUAGE_PRESETS, NotificationPanelComponent, PanoramaComponent, PortalLayoutComponent, PortalService, SidebarComponent, TILE_DIMENSIONS, TileComponent, TileSize, clearStatusBar, createAvatarMenu, createBlade, createCommand, createDataBlade, createDetailCommands, createNavItem, createNotificationPanel, createPanorama, createTile, executeDeleteItem, executeLoadItem, executeLoadItems, executeSaveItem, filterItems, getUserDisplayName, layoutTiles, nextBladeUid, provideBladeRouter, providePortalAzure, registerLanguagePreset, statusBarError, statusBarInfo, statusBarSuccess };
|
|
2414
2593
|
//# sourceMappingURL=ardimedia-angular-portal-azure.mjs.map
|