@ardimedia/angular-portal-azure 0.3.25 → 0.3.27
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).
|
|
@@ -1857,28 +1979,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImpor
|
|
|
1857
1979
|
*
|
|
1858
1980
|
* Usage:
|
|
1859
1981
|
* ```html
|
|
1982
|
+
* <!-- Default: each component is wrapped in <apa-blade> -->
|
|
1860
1983
|
* <apa-blade-host />
|
|
1984
|
+
*
|
|
1985
|
+
* <!-- No wrapper: components render directly (they manage their own blade chrome) -->
|
|
1986
|
+
* <apa-blade-host [wrapBlade]="false" />
|
|
1861
1987
|
* ```
|
|
1862
1988
|
*/
|
|
1863
1989
|
class BladeHostComponent {
|
|
1990
|
+
/** Whether to wrap each component in an `<apa-blade>` element. Default: true. */
|
|
1991
|
+
wrapBlade = input(true, ...(ngDevMode ? [{ debugName: "wrapBlade" }] : /* istanbul ignore next */ []));
|
|
1864
1992
|
portal = inject(PortalService);
|
|
1865
1993
|
registry = inject(BladeRegistry);
|
|
1866
1994
|
getComponent(path) {
|
|
1867
1995
|
return this.registry.get(path) ?? null;
|
|
1868
1996
|
}
|
|
1869
1997
|
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: `
|
|
1998
|
+
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
1999
|
<div id="apa-blade-area" class="fxs-journey-target fxs-journey">
|
|
1872
2000
|
<div class="fxs-journey-layout fxs-stacklayout fxs-stacklayout-horizontal">
|
|
1873
2001
|
@for (blade of portal.blades(); track blade.uid) {
|
|
1874
2002
|
<div class="azureportalblade fxs-stacklayout-child">
|
|
1875
|
-
|
|
2003
|
+
@if (wrapBlade()) {
|
|
2004
|
+
<apa-blade [blade]="blade">
|
|
2005
|
+
@if (getComponent(blade.path); as component) {
|
|
2006
|
+
<ng-container *ngComponentOutlet="component" />
|
|
2007
|
+
} @else {
|
|
2008
|
+
<p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
|
|
2009
|
+
}
|
|
2010
|
+
</apa-blade>
|
|
2011
|
+
} @else {
|
|
1876
2012
|
@if (getComponent(blade.path); as component) {
|
|
1877
2013
|
<ng-container *ngComponentOutlet="component" />
|
|
1878
2014
|
} @else {
|
|
1879
2015
|
<p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
|
|
1880
2016
|
}
|
|
1881
|
-
|
|
2017
|
+
}
|
|
1882
2018
|
</div>
|
|
1883
2019
|
}
|
|
1884
2020
|
</div>
|
|
@@ -1892,19 +2028,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImpor
|
|
|
1892
2028
|
<div class="fxs-journey-layout fxs-stacklayout fxs-stacklayout-horizontal">
|
|
1893
2029
|
@for (blade of portal.blades(); track blade.uid) {
|
|
1894
2030
|
<div class="azureportalblade fxs-stacklayout-child">
|
|
1895
|
-
|
|
2031
|
+
@if (wrapBlade()) {
|
|
2032
|
+
<apa-blade [blade]="blade">
|
|
2033
|
+
@if (getComponent(blade.path); as component) {
|
|
2034
|
+
<ng-container *ngComponentOutlet="component" />
|
|
2035
|
+
} @else {
|
|
2036
|
+
<p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
|
|
2037
|
+
}
|
|
2038
|
+
</apa-blade>
|
|
2039
|
+
} @else {
|
|
1896
2040
|
@if (getComponent(blade.path); as component) {
|
|
1897
2041
|
<ng-container *ngComponentOutlet="component" />
|
|
1898
2042
|
} @else {
|
|
1899
2043
|
<p style="padding:25px; color:var(--apa-text-secondary);">{{ blade.path }}</p>
|
|
1900
2044
|
}
|
|
1901
|
-
|
|
2045
|
+
}
|
|
1902
2046
|
</div>
|
|
1903
2047
|
}
|
|
1904
2048
|
</div>
|
|
1905
2049
|
</div>
|
|
1906
2050
|
`, styles: [":host{display:block;height:100%}\n"] }]
|
|
1907
|
-
}] });
|
|
2051
|
+
}], propDecorators: { wrapBlade: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrapBlade", required: false }] }] } });
|
|
1908
2052
|
|
|
1909
2053
|
/**
|
|
1910
2054
|
* Navigation blade content — renders a list of nav items.
|
|
@@ -2145,8 +2289,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImpor
|
|
|
2145
2289
|
*/
|
|
2146
2290
|
class BladeDetailComponent {
|
|
2147
2291
|
blade = input.required(...(ngDevMode ? [{ debugName: "blade" }] : /* istanbul ignore next */ []));
|
|
2292
|
+
bladeService = inject(BladeService);
|
|
2293
|
+
/** The template-driven form projected into this detail, if any (auto-discovered). */
|
|
2294
|
+
form = contentChild(NgForm, { ...(ngDevMode ? { debugName: "form" } : /* istanbul ignore next */ {}), descendants: true });
|
|
2295
|
+
ngAfterContentInit() {
|
|
2296
|
+
const blade = this.blade();
|
|
2297
|
+
const isDirty = () => this.form()?.dirty ?? false;
|
|
2298
|
+
// Expose on the lifecycle (for consumers) and register with the BladeService so navigation
|
|
2299
|
+
// away from a dirty blade prompts for confirmation. No per-detail wiring needed — every
|
|
2300
|
+
// detail that projects a <form> is guarded automatically.
|
|
2301
|
+
blade.lifecycle.isDirty = isDirty;
|
|
2302
|
+
this.bladeService.registerDirtyCheck(blade.path, isDirty);
|
|
2303
|
+
// After a successful save, reset the form to pristine so the guard does not fire on a
|
|
2304
|
+
// saved-but-not-yet-navigated blade. Wrap (don't replace) any existing onSavedItem hook.
|
|
2305
|
+
const originalOnSaved = blade.lifecycle.onSavedItem;
|
|
2306
|
+
blade.lifecycle.onSavedItem = () => {
|
|
2307
|
+
this.form()?.form.markAsPristine();
|
|
2308
|
+
originalOnSaved?.();
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
ngOnDestroy() {
|
|
2312
|
+
this.bladeService.unregisterDirtyCheck(this.blade().path);
|
|
2313
|
+
}
|
|
2148
2314
|
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.
|
|
2315
|
+
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
2316
|
<div class="apa-blade-detail">
|
|
2151
2317
|
<ng-content />
|
|
2152
2318
|
</div>
|
|
@@ -2159,7 +2325,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImpor
|
|
|
2159
2325
|
<ng-content />
|
|
2160
2326
|
</div>
|
|
2161
2327
|
`, 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 }] }] } });
|
|
2328
|
+
}], 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
2329
|
/**
|
|
2164
2330
|
* Create standard detail blade commands (new, save, delete, cancel).
|
|
2165
2331
|
* Convenience function for setting up typical detail/edit blade commands
|
|
@@ -2410,5 +2576,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.3", ngImpor
|
|
|
2410
2576
|
* Generated bundle index. Do not edit.
|
|
2411
2577
|
*/
|
|
2412
2578
|
|
|
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 };
|
|
2579
|
+
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
2580
|
//# sourceMappingURL=ardimedia-angular-portal-azure.mjs.map
|