@memberjunction/ng-shared 5.51.0 → 6.1.0-edge.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.
Files changed (41) hide show
  1. package/LICENSE +7 -0
  2. package/dist/lib/base-dashboard.d.ts +16 -0
  3. package/dist/lib/base-dashboard.d.ts.map +1 -1
  4. package/dist/lib/base-dashboard.js +36 -5
  5. package/dist/lib/base-dashboard.js.map +1 -1
  6. package/dist/lib/base-resource-component.d.ts +33 -3
  7. package/dist/lib/base-resource-component.d.ts.map +1 -1
  8. package/dist/lib/base-resource-component.js +65 -11
  9. package/dist/lib/base-resource-component.js.map +1 -1
  10. package/dist/lib/explorer-breakpoint.service.d.ts +39 -0
  11. package/dist/lib/explorer-breakpoint.service.d.ts.map +1 -0
  12. package/dist/lib/explorer-breakpoint.service.js +57 -0
  13. package/dist/lib/explorer-breakpoint.service.js.map +1 -0
  14. package/dist/lib/navigation.interfaces.d.ts +13 -0
  15. package/dist/lib/navigation.interfaces.d.ts.map +1 -1
  16. package/dist/lib/navigation.interfaces.js.map +1 -1
  17. package/dist/lib/navigation.service.d.ts +70 -3
  18. package/dist/lib/navigation.service.d.ts.map +1 -1
  19. package/dist/lib/navigation.service.js +356 -22
  20. package/dist/lib/navigation.service.js.map +1 -1
  21. package/dist/lib/record-open-style.d.ts +88 -0
  22. package/dist/lib/record-open-style.d.ts.map +1 -0
  23. package/dist/lib/record-open-style.js +124 -0
  24. package/dist/lib/record-open-style.js.map +1 -0
  25. package/dist/lib/record-tab-icon.d.ts +10 -0
  26. package/dist/lib/record-tab-icon.d.ts.map +1 -0
  27. package/dist/lib/record-tab-icon.js +15 -0
  28. package/dist/lib/record-tab-icon.js.map +1 -0
  29. package/dist/lib/safe-detect-changes.d.ts +18 -0
  30. package/dist/lib/safe-detect-changes.d.ts.map +1 -0
  31. package/dist/lib/safe-detect-changes.js +29 -0
  32. package/dist/lib/safe-detect-changes.js.map +1 -0
  33. package/dist/lib/shared.service.d.ts +5 -0
  34. package/dist/lib/shared.service.d.ts.map +1 -1
  35. package/dist/lib/shared.service.js +16 -10
  36. package/dist/lib/shared.service.js.map +1 -1
  37. package/dist/public-api.d.ts +4 -0
  38. package/dist/public-api.d.ts.map +1 -1
  39. package/dist/public-api.js +4 -0
  40. package/dist/public-api.js.map +1 -1
  41. package/package.json +19 -17
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2023 MemberJunction
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -40,8 +40,24 @@ export declare abstract class BaseDashboard extends BaseResourceComponent implem
40
40
  ngOnDestroy(): void;
41
41
  /**
42
42
  * This method will result in the dashboard being reloaded.
43
+ *
44
+ * NOTE — this promise does NOT reject when the reload fails. A load error is caught, logged via
45
+ * `LogError` and surfaced on the {@link Error} output (see {@link runGuardedLoad}), and the
46
+ * promise then RESOLVES. That is deliberate: callers invoke `Refresh()` fire-and-forget (the
47
+ * container does), so rejecting here would produce an unhandled rejection rather than reaching
48
+ * anyone. If you need to know whether a refresh succeeded, subscribe to {@link Error} — do not
49
+ * rely on `await Refresh()` throwing.
43
50
  */
44
51
  Refresh(): Promise<void>;
52
+ /**
53
+ * Runs a load phase (initial mount or Refresh) with the GUARANTEE that {@link NotifyLoadComplete}
54
+ * always fires — even if the work throws. The Explorer shell's loading screen blocks on that
55
+ * signal, so without this guard one dashboard's uncaught `initDashboard()`/`loadData()` error
56
+ * (e.g. an in-flight query rejecting while MJAPI restarts) would hang the entire shell forever on
57
+ * a direct/deep-link load. On error we log it and emit the {@link Error} output so the subclass or
58
+ * container can render its own error state; the loading screen still clears either way.
59
+ */
60
+ private runGuardedLoad;
45
61
  private _visible;
46
62
  /**
47
63
  * This method can be used by a container to let the dashboard know that it is being opened/closed. Base class just sets a flag.
@@ -1 +1 @@
1
- {"version":3,"file":"base-dashboard.d.ts","sourceRoot":"","sources":["../../src/lib/base-dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,YAAY,EAAS,SAAS,EAAE,MAAM,EAAU,MAAM,eAAe,CAAC;AAC1F,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AACxF,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;;AAElE,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,yBAAyB,CAAC;IACrC,SAAS,CAAC,EAAE,GAAG,CAAC;CACjB;AAED;;GAEG;AACH,8BACsB,aAAc,SAAQ,qBAAsB,YAAW,MAAM,EAAE,SAAS;IAC5F;;OAEG;IACH,IAAa,MAAM,CAAC,KAAK,EAAE,eAAe,EAEzC;IAED,IAAI,MAAM,IAAI,eAAe,GAAG,IAAI,CAEnC;IAED;;OAEG;IACO,KAAK,sBAA6B;IAE5C;;OAEG;IACO,gBAAgB,oBAA2B;IAErD;;OAEG;IACO,WAAW,oBAA2B;IAEhD;;OAEG;IACO,gBAAgB;oBAAiC,MAAM;oBAAc,YAAY;OAAK;IAEhG,SAAS,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAAQ;IAE3C,QAAQ;IAOd,WAAW,IAAI,IAAI;IAInB;;OAEG;IACU,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAKrC,OAAO,CAAC,QAAQ,CAAkB;IAClC;;OAEG;IACI,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIzC;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,aAAa,IAAI,IAAI;IAExC;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,QAAQ,IAAI,IAAI;IAEnC;;;;OAIG;IACG,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;yCA5E3C,aAAa;2CAAb,aAAa;CA+ElC"}
1
+ {"version":3,"file":"base-dashboard.d.ts","sourceRoot":"","sources":["../../src/lib/base-dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,YAAY,EAAS,SAAS,EAAE,MAAM,EAAU,MAAM,eAAe,CAAC;AAC1F,OAAO,EAAE,YAAY,EAAY,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,yBAAyB,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AACxF,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;;AAElE,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,yBAAyB,CAAC;IACrC,SAAS,CAAC,EAAE,GAAG,CAAC;CACjB;AAED;;GAEG;AACH,8BACsB,aAAc,SAAQ,qBAAsB,YAAW,MAAM,EAAE,SAAS;IAC5F;;OAEG;IACH,IAAa,MAAM,CAAC,KAAK,EAAE,eAAe,EAEzC;IAED,IAAI,MAAM,IAAI,eAAe,GAAG,IAAI,CAEnC;IAED;;OAEG;IACO,KAAK,sBAA6B;IAE5C;;OAEG;IACO,gBAAgB,oBAA2B;IAErD;;OAEG;IACO,WAAW,oBAA2B;IAEhD;;OAEG;IACO,gBAAgB;oBAAiC,MAAM;oBAAc,YAAY;OAAK;IAEhG,SAAS,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAAQ;IAE3C,QAAQ;IAUd,WAAW,IAAI,IAAI;IAInB;;;;;;;;;OASG;IACU,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrC;;;;;;;OAOG;YACW,cAAc;IAY5B,OAAO,CAAC,QAAQ,CAAkB;IAClC;;OAEG;IACI,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIzC;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,aAAa,IAAI,IAAI;IAExC;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,QAAQ,IAAI,IAAI;IAEnC;;;;OAIG;IACG,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;yCAzG3C,aAAa;2CAAb,aAAa;CA4GlC"}
@@ -1,4 +1,5 @@
1
1
  import { Directive, EventEmitter, Input, Output } from '@angular/core';
2
+ import { LogError } from '@memberjunction/core';
2
3
  import { BaseResourceComponent } from './base-resource-component';
3
4
  import * as i0 from "@angular/core";
4
5
  /**
@@ -33,19 +34,49 @@ export class BaseDashboard extends BaseResourceComponent {
33
34
  _config = null;
34
35
  async ngOnInit() {
35
36
  super.ngOnInit();
36
- this.initDashboard();
37
- await this.loadData();
38
- this.NotifyLoadComplete();
37
+ // initDashboard() runs before loadData() and can throw too, so BOTH are inside the guard — a
38
+ // failure in either still reaches NotifyLoadComplete() (see runGuardedLoad).
39
+ await this.runGuardedLoad(async () => {
40
+ this.initDashboard();
41
+ await this.loadData();
42
+ });
39
43
  }
40
44
  ngOnDestroy() {
41
45
  super.ngOnDestroy();
42
46
  }
43
47
  /**
44
48
  * This method will result in the dashboard being reloaded.
49
+ *
50
+ * NOTE — this promise does NOT reject when the reload fails. A load error is caught, logged via
51
+ * `LogError` and surfaced on the {@link Error} output (see {@link runGuardedLoad}), and the
52
+ * promise then RESOLVES. That is deliberate: callers invoke `Refresh()` fire-and-forget (the
53
+ * container does), so rejecting here would produce an unhandled rejection rather than reaching
54
+ * anyone. If you need to know whether a refresh succeeded, subscribe to {@link Error} — do not
55
+ * rely on `await Refresh()` throwing.
45
56
  */
46
57
  async Refresh() {
47
- await this.loadData();
48
- this.NotifyLoadComplete();
58
+ await this.runGuardedLoad(() => this.loadData());
59
+ }
60
+ /**
61
+ * Runs a load phase (initial mount or Refresh) with the GUARANTEE that {@link NotifyLoadComplete}
62
+ * always fires — even if the work throws. The Explorer shell's loading screen blocks on that
63
+ * signal, so without this guard one dashboard's uncaught `initDashboard()`/`loadData()` error
64
+ * (e.g. an in-flight query rejecting while MJAPI restarts) would hang the entire shell forever on
65
+ * a direct/deep-link load. On error we log it and emit the {@link Error} output so the subclass or
66
+ * container can render its own error state; the loading screen still clears either way.
67
+ */
68
+ async runGuardedLoad(work) {
69
+ try {
70
+ await work();
71
+ }
72
+ catch (err) {
73
+ const error = err instanceof Error ? err : new Error(String(err));
74
+ LogError(error);
75
+ this.Error.emit(error);
76
+ }
77
+ finally {
78
+ this.NotifyLoadComplete();
79
+ }
49
80
  }
50
81
  _visible = false;
51
82
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"base-dashboard.js","sourceRoot":"","sources":["../../src/lib/base-dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAqB,MAAM,EAAE,MAAM,eAAe,CAAC;AAG1F,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;;AAOlE;;GAEG;AAEH,MAAM,OAAgB,aAAc,SAAQ,qBAAqB;IAC/D;;OAEG;IACH,IAAa,MAAM,CAAC,KAAsB;QACxC,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;IACvB,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED;;OAEG;IACO,KAAK,GAAG,IAAI,YAAY,EAAS,CAAC;IAE5C;;OAEG;IACO,gBAAgB,GAAG,IAAI,YAAY,EAAO,CAAC;IAErD;;OAEG;IACO,WAAW,GAAG,IAAI,YAAY,EAAO,CAAC;IAEhD;;OAEG;IACO,gBAAgB,GAAG,IAAI,YAAY,EAAkD,CAAC;IAEtF,OAAO,GAA2B,IAAI,CAAC;IAEjD,KAAK,CAAC,QAAQ;QACZ,KAAK,CAAC,QAAQ,EAAE,CAAC;QACjB,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACtB,IAAI,CAAC,kBAAkB,EAAE,CAAC;IAC5B,CAAC;IAED,WAAW;QACT,KAAK,CAAC,WAAW,EAAE,CAAC;IACtB,CAAC;IAED;;OAEG;IACI,KAAK,CAAC,OAAO;QAClB,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACtB,IAAI,CAAC,kBAAkB,EAAE,CAAC;IAC5B,CAAC;IAEO,QAAQ,GAAY,KAAK,CAAC;IAClC;;OAEG;IACI,UAAU,CAAC,OAAgB;QAChC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAYD;;;;OAIG;IACH,KAAK,CAAC,oBAAoB,CAAC,IAAkB;QAC3C,OAAO,EAAE,CAAC;IACZ,CAAC;iOA9EmB,aAAa,yBAAb,aAAa;6DAAb,aAAa;;iFAAb,aAAa;cADlC,SAAS;;kBAKP,KAAK;;kBAWL,MAAM;;kBAKN,MAAM;;kBAKN,MAAM;;kBAKN,MAAM","sourcesContent":["import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';\nimport { CompositeKey } from '@memberjunction/core';\nimport { MJDashboardEntityExtended, ResourceData } from '@memberjunction/core-entities';\nimport { BaseResourceComponent } from './base-resource-component';\n\nexport interface DashboardConfig {\n dashboard: MJDashboardEntityExtended;\n userState?: any;\n}\n\n/**\n * Make the base class a directive so we can use Angular functionality and sub-classes can be @Component \n */\n@Directive()\nexport abstract class BaseDashboard extends BaseResourceComponent implements OnInit, OnDestroy {\n /**\n * Set or change the dashboard configuration. Changing this property will NOT cause the dashboard to reload. Call Refresh() to do that.\n */\n @Input() set Config(value: DashboardConfig) {\n this._config = value;\n }\n\n get Config(): DashboardConfig | null {\n return this._config;\n }\n\n /**\n * Subclasses can emit anytime an error occurs. \n */\n @Output() Error = new EventEmitter<Error>();\n\n /**\n * Subclasses should emit this event anytime their internal state changes in a way that they'd like to persist.\n */\n @Output() UserStateChanged = new EventEmitter<any>();\n\n /**\n * Subclasses can emit this event anytime they want to communicate with the container to let it know that something has happened of significance.\n */\n @Output() Interaction = new EventEmitter<any>();\n\n /**\n * Subclasses can emit this event anytime they want to open a record within a particular entity. The container should handle this event and open the record.\n */\n @Output() OpenEntityRecord = new EventEmitter<{EntityName: string, RecordPKey: CompositeKey}>();\n\n protected _config: DashboardConfig | null = null;\n\n async ngOnInit() {\n super.ngOnInit();\n this.initDashboard();\n await this.loadData();\n this.NotifyLoadComplete();\n }\n\n ngOnDestroy(): void {\n super.ngOnDestroy();\n }\n\n /**\n * This method will result in the dashboard being reloaded.\n */\n public async Refresh(): Promise<void> {\n await this.loadData();\n this.NotifyLoadComplete();\n }\n\n private _visible: boolean = false;\n /**\n * This method can be used by a container to let the dashboard know that it is being opened/closed. Base class just sets a flag.\n */\n public SetVisible(visible: boolean): void {\n this._visible = visible;\n }\n\n /**\n * Subclasses can override this method to perform any initialization they need. This method only runs once when the dashboard is created.\n */\n protected abstract initDashboard(): void;\n\n /**\n * Subclasses should override this method to load their data. This method is called when the dashboard is created and when Refresh() is called.\n */\n protected abstract loadData(): void;\n\n /**\n * Sub-classes can override this to provide a custom icon class\n * @param data \n * @returns \n */\n async GetResourceIconClass(data: ResourceData): Promise<string> {\n return \"\";\n }\n} "]}
1
+ {"version":3,"file":"base-dashboard.js","sourceRoot":"","sources":["../../src/lib/base-dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAqB,MAAM,EAAE,MAAM,eAAe,CAAC;AAC1F,OAAO,EAAgB,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAE9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;;AAOlE;;GAEG;AAEH,MAAM,OAAgB,aAAc,SAAQ,qBAAqB;IAC/D;;OAEG;IACH,IAAa,MAAM,CAAC,KAAsB;QACxC,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;IACvB,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED;;OAEG;IACO,KAAK,GAAG,IAAI,YAAY,EAAS,CAAC;IAE5C;;OAEG;IACO,gBAAgB,GAAG,IAAI,YAAY,EAAO,CAAC;IAErD;;OAEG;IACO,WAAW,GAAG,IAAI,YAAY,EAAO,CAAC;IAEhD;;OAEG;IACO,gBAAgB,GAAG,IAAI,YAAY,EAAkD,CAAC;IAEtF,OAAO,GAA2B,IAAI,CAAC;IAEjD,KAAK,CAAC,QAAQ;QACZ,KAAK,CAAC,QAAQ,EAAE,CAAC;QACjB,6FAA6F;QAC7F,6EAA6E;QAC7E,MAAM,IAAI,CAAC,cAAc,CAAC,KAAK,IAAI,EAAE;YACnC,IAAI,CAAC,aAAa,EAAE,CAAC;YACrB,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACxB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,WAAW;QACT,KAAK,CAAC,WAAW,EAAE,CAAC;IACtB,CAAC;IAED;;;;;;;;;OASG;IACI,KAAK,CAAC,OAAO;QAClB,MAAM,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IACnD,CAAC;IAED;;;;;;;OAOG;IACK,KAAK,CAAC,cAAc,CAAC,IAAgC;QAC3D,IAAI,CAAC;YACH,MAAM,IAAI,EAAE,CAAC;QACf,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,KAAK,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YAClE,QAAQ,CAAC,KAAK,CAAC,CAAC;YAChB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC5B,CAAC;IACH,CAAC;IAEO,QAAQ,GAAY,KAAK,CAAC;IAClC;;OAEG;IACI,UAAU,CAAC,OAAgB;QAChC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAYD;;;;OAIG;IACH,KAAK,CAAC,oBAAoB,CAAC,IAAkB;QAC3C,OAAO,EAAE,CAAC;IACZ,CAAC;iOA3GmB,aAAa,yBAAb,aAAa;6DAAb,aAAa;;iFAAb,aAAa;cADlC,SAAS;;kBAKP,KAAK;;kBAWL,MAAM;;kBAKN,MAAM;;kBAKN,MAAM;;kBAKN,MAAM","sourcesContent":["import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';\nimport { CompositeKey, LogError } from '@memberjunction/core';\nimport { MJDashboardEntityExtended, ResourceData } from '@memberjunction/core-entities';\nimport { BaseResourceComponent } from './base-resource-component';\n\nexport interface DashboardConfig {\n dashboard: MJDashboardEntityExtended;\n userState?: any;\n}\n\n/**\n * Make the base class a directive so we can use Angular functionality and sub-classes can be @Component \n */\n@Directive()\nexport abstract class BaseDashboard extends BaseResourceComponent implements OnInit, OnDestroy {\n /**\n * Set or change the dashboard configuration. Changing this property will NOT cause the dashboard to reload. Call Refresh() to do that.\n */\n @Input() set Config(value: DashboardConfig) {\n this._config = value;\n }\n\n get Config(): DashboardConfig | null {\n return this._config;\n }\n\n /**\n * Subclasses can emit anytime an error occurs. \n */\n @Output() Error = new EventEmitter<Error>();\n\n /**\n * Subclasses should emit this event anytime their internal state changes in a way that they'd like to persist.\n */\n @Output() UserStateChanged = new EventEmitter<any>();\n\n /**\n * Subclasses can emit this event anytime they want to communicate with the container to let it know that something has happened of significance.\n */\n @Output() Interaction = new EventEmitter<any>();\n\n /**\n * Subclasses can emit this event anytime they want to open a record within a particular entity. The container should handle this event and open the record.\n */\n @Output() OpenEntityRecord = new EventEmitter<{EntityName: string, RecordPKey: CompositeKey}>();\n\n protected _config: DashboardConfig | null = null;\n\n async ngOnInit() {\n super.ngOnInit();\n // initDashboard() runs before loadData() and can throw too, so BOTH are inside the guard — a\n // failure in either still reaches NotifyLoadComplete() (see runGuardedLoad).\n await this.runGuardedLoad(async () => {\n this.initDashboard();\n await this.loadData();\n });\n }\n\n ngOnDestroy(): void {\n super.ngOnDestroy();\n }\n\n /**\n * This method will result in the dashboard being reloaded.\n *\n * NOTE — this promise does NOT reject when the reload fails. A load error is caught, logged via\n * `LogError` and surfaced on the {@link Error} output (see {@link runGuardedLoad}), and the\n * promise then RESOLVES. That is deliberate: callers invoke `Refresh()` fire-and-forget (the\n * container does), so rejecting here would produce an unhandled rejection rather than reaching\n * anyone. If you need to know whether a refresh succeeded, subscribe to {@link Error} — do not\n * rely on `await Refresh()` throwing.\n */\n public async Refresh(): Promise<void> {\n await this.runGuardedLoad(() => this.loadData());\n }\n\n /**\n * Runs a load phase (initial mount or Refresh) with the GUARANTEE that {@link NotifyLoadComplete}\n * always fires — even if the work throws. The Explorer shell's loading screen blocks on that\n * signal, so without this guard one dashboard's uncaught `initDashboard()`/`loadData()` error\n * (e.g. an in-flight query rejecting while MJAPI restarts) would hang the entire shell forever on\n * a direct/deep-link load. On error we log it and emit the {@link Error} output so the subclass or\n * container can render its own error state; the loading screen still clears either way.\n */\n private async runGuardedLoad(work: () => void | Promise<void>): Promise<void> {\n try {\n await work();\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n LogError(error);\n this.Error.emit(error);\n } finally {\n this.NotifyLoadComplete();\n }\n }\n\n private _visible: boolean = false;\n /**\n * This method can be used by a container to let the dashboard know that it is being opened/closed. Base class just sets a flag.\n */\n public SetVisible(visible: boolean): void {\n this._visible = visible;\n }\n\n /**\n * Subclasses can override this method to perform any initialization they need. This method only runs once when the dashboard is created.\n */\n protected abstract initDashboard(): void;\n\n /**\n * Subclasses should override this method to load their data. This method is called when the dashboard is created and when Refresh() is called.\n */\n protected abstract loadData(): void;\n\n /**\n * Sub-classes can override this to provide a custom icon class\n * @param data \n * @returns \n */\n async GetResourceIconClass(data: ResourceData): Promise<string> {\n return \"\";\n }\n} "]}
@@ -13,6 +13,14 @@ export declare abstract class BaseResourceComponent extends BaseNavigationCompon
13
13
  private _lastDeliveredParamsKey;
14
14
  protected destroy$: Subject<void>;
15
15
  protected navigationService: NavigationService;
16
+ /**
17
+ * Optional: the tab container instantiates LIGHTWEIGHT throwaway instances
18
+ * of resource components (via runInInjectionContext with an environment
19
+ * injector) purely to call GetResourceDisplayName for background tab
20
+ * titles. ChangeDetectorRef only exists in node injectors, so a required
21
+ * inject() there throws NG0201 and silently kills every background title
22
+ * resolution — tabs keep raw "Entity - ID|guid" titles until opened.
23
+ */
16
24
  private changeDetectorRef;
17
25
  /**
18
26
  * Tab ID for query param notification scoping. Set by resource wrappers
@@ -20,15 +28,29 @@ export declare abstract class BaseResourceComponent extends BaseNavigationCompon
20
28
  * If not set, falls back to Data.Configuration.tabId.
21
29
  */
22
30
  ParentTabId: string | null;
31
+ /**
32
+ * Tab id this component was RE-HOMED to by a cache reattach. The
33
+ * component cache keys on driver+record+app — NOT tab id — so a cached
34
+ * component can be reattached to a different tab than it was born under.
35
+ * Without rebinding, its query-param subscription listens to the DEAD
36
+ * birth tab forever and every delivery to the live tab is lost (the
37
+ * "crumb lands on the dashboard root after promote/demote" bug).
38
+ */
39
+ private reboundTabId;
40
+ /** Handle for the reactive param subscription so RebindTabId can replace it */
41
+ private reactiveParamSub;
23
42
  get Data(): ResourceData;
24
43
  set Data(value: ResourceData);
25
44
  private _loadComplete;
26
45
  get LoadComplete(): boolean;
27
46
  /**
28
47
  * Watchdog window: if a resource component hasn't called {@link NotifyLoadComplete} within this
29
- * time, we log a warning naming the offending class. The app loading screen waits on
30
- * NotifyLoadComplete, so a resource that never calls it hangs the screen forever this makes the
31
- * culprit obvious in the console instead of leaving a mystery "stuck loading" state.
48
+ * time, we log a warning naming the offending class AND fail open forcing NotifyLoadComplete
49
+ * so the app loading screen (which blocks on that signal) is released. Without the fail-open, a
50
+ * resource that never signals because it errored, hung, or has its own `ngOnInit` that
51
+ * bypasses BaseDashboard's guarded lifecycle — would hang the whole Explorer forever. This is
52
+ * the safety net covering EVERY BaseResourceComponent subclass, not just BaseDashboard (whose
53
+ * own try/finally already guarantees the signal on the normal path).
32
54
  */
33
55
  private static readonly LOAD_COMPLETE_WATCHDOG_MS;
34
56
  private _loadCompleteWatchdog;
@@ -97,6 +119,14 @@ export declare abstract class BaseResourceComponent extends BaseNavigationCompon
97
119
  * wrappers for child dashboards), then falls back to Data.Configuration.tabId.
98
120
  */
99
121
  getTabId(): string;
122
+ /**
123
+ * Re-home this component to a different tab (cache reattach across tab
124
+ * ids). Replaces the reactive param subscription with one bound to the
125
+ * new tab and resets the duplicate-delivery key so the new tab's
126
+ * CURRENT params apply as a fresh mount would — the replay-on-subscribe
127
+ * delivers them immediately.
128
+ */
129
+ RebindTabId(tabId: string): void;
100
130
  private getQueryParamUpdateGuard;
101
131
  protected NotifyLoadComplete(): void;
102
132
  protected NotifyLoadStarted(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"base-resource-component.d.ts","sourceRoot":"","sources":["../../src/lib/base-resource-component.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"base-resource-component.d.ts","sourceRoot":"","sources":["../../src/lib/base-resource-component.ts"],"names":[],"mappings":"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"}
@@ -13,6 +13,14 @@ export class BaseResourceComponent extends BaseNavigationComponent {
13
13
  _lastDeliveredParamsKey = null;
14
14
  destroy$ = new Subject();
15
15
  navigationService = inject(NavigationService);
16
+ /**
17
+ * Optional: the tab container instantiates LIGHTWEIGHT throwaway instances
18
+ * of resource components (via runInInjectionContext with an environment
19
+ * injector) purely to call GetResourceDisplayName for background tab
20
+ * titles. ChangeDetectorRef only exists in node injectors, so a required
21
+ * inject() there throws NG0201 and silently kills every background title
22
+ * resolution — tabs keep raw "Entity - ID|guid" titles until opened.
23
+ */
16
24
  changeDetectorRef = inject(ChangeDetectorRef, { optional: true });
17
25
  /**
18
26
  * Tab ID for query param notification scoping. Set by resource wrappers
@@ -20,6 +28,17 @@ export class BaseResourceComponent extends BaseNavigationComponent {
20
28
  * If not set, falls back to Data.Configuration.tabId.
21
29
  */
22
30
  ParentTabId = null;
31
+ /**
32
+ * Tab id this component was RE-HOMED to by a cache reattach. The
33
+ * component cache keys on driver+record+app — NOT tab id — so a cached
34
+ * component can be reattached to a different tab than it was born under.
35
+ * Without rebinding, its query-param subscription listens to the DEAD
36
+ * birth tab forever and every delivery to the live tab is lost (the
37
+ * "crumb lands on the dashboard root after promote/demote" bug).
38
+ */
39
+ reboundTabId = null;
40
+ /** Handle for the reactive param subscription so RebindTabId can replace it */
41
+ reactiveParamSub = null;
23
42
  get Data() {
24
43
  return this._data;
25
44
  }
@@ -32,9 +51,12 @@ export class BaseResourceComponent extends BaseNavigationComponent {
32
51
  }
33
52
  /**
34
53
  * Watchdog window: if a resource component hasn't called {@link NotifyLoadComplete} within this
35
- * time, we log a warning naming the offending class. The app loading screen waits on
36
- * NotifyLoadComplete, so a resource that never calls it hangs the screen forever this makes the
37
- * culprit obvious in the console instead of leaving a mystery "stuck loading" state.
54
+ * time, we log a warning naming the offending class AND fail open forcing NotifyLoadComplete
55
+ * so the app loading screen (which blocks on that signal) is released. Without the fail-open, a
56
+ * resource that never signals because it errored, hung, or has its own `ngOnInit` that
57
+ * bypasses BaseDashboard's guarded lifecycle — would hang the whole Explorer forever. This is
58
+ * the safety net covering EVERY BaseResourceComponent subclass, not just BaseDashboard (whose
59
+ * own try/finally already guarantees the signal on the normal path).
38
60
  */
39
61
  static LOAD_COMPLETE_WATCHDOG_MS = 15_000;
40
62
  _loadCompleteWatchdog = null;
@@ -89,9 +111,17 @@ export class BaseResourceComponent extends BaseNavigationComponent {
89
111
  if (!this._loadComplete) {
90
112
  // eslint-disable-next-line no-console
91
113
  console.warn(`[LoadComplete WATCHDOG] ${this.constructor.name} has NOT called NotifyLoadComplete() ` +
92
- `within ${BaseResourceComponent.LOAD_COMPLETE_WATCHDOG_MS / 1000}s. The app loading screen ` +
93
- `waits on this signal if the screen is stuck, this resource is the likely cause. ` +
114
+ `within ${BaseResourceComponent.LOAD_COMPLETE_WATCHDOG_MS / 1000}s FAILING OPEN: forcing ` +
115
+ `load-complete to release the app loading screen. This resource either errored/hung without ` +
116
+ `signalling, or is simply still loading (a slow-but-healthy load re-signals harmlessly when ` +
117
+ `it finishes). If you are chasing a "stuck loading" report, start here. ` +
94
118
  `(tabId=${this.getTabId() || 'n/a'})`);
119
+ // Fail-open: the shell's loading screen blocks on NotifyLoadComplete. A subclass that
120
+ // never calls it — one whose own ngOnInit bypasses BaseDashboard's guarded lifecycle,
121
+ // or whose load genuinely hangs — would otherwise brick the whole Explorer. Force the
122
+ // signal so no single resource can take down the shell. (Idempotent-safe: NotifyLoad
123
+ // Complete is already called repeatedly on the normal path, e.g. every Refresh.)
124
+ this.NotifyLoadComplete();
95
125
  }
96
126
  }, BaseResourceComponent.LOAD_COMPLETE_WATCHDOG_MS);
97
127
  }
@@ -145,7 +175,7 @@ export class BaseResourceComponent extends BaseNavigationComponent {
145
175
  const myTabId = this.getTabId();
146
176
  return !myTabId || event.TabId === myTabId;
147
177
  }), takeUntil(this.destroy$))
148
- .subscribe(event => this.deliverQueryParams(event.Params));
178
+ .subscribe(event => this.deliverQueryParams(event.Params, event.Force === true));
149
179
  }
150
180
  /**
151
181
  * Internal: reactively deliver this tab's query params from the workspace
@@ -160,7 +190,8 @@ export class BaseResourceComponent extends BaseNavigationComponent {
160
190
  if (!tabId) {
161
191
  return; // No tab scope (e.g. embedded usage) — nothing to observe.
162
192
  }
163
- this.navigationService.ObserveTabQueryParams(tabId)
193
+ this.reactiveParamSub?.unsubscribe();
194
+ this.reactiveParamSub = this.navigationService.ObserveTabQueryParams(tabId)
164
195
  .pipe(takeUntil(this.destroy$))
165
196
  .subscribe(params => this.deliverQueryParams(params));
166
197
  }
@@ -169,16 +200,23 @@ export class BaseResourceComponent extends BaseNavigationComponent {
169
200
  * identical deliveries (the two paths overlap on back/forward) and labels the first
170
201
  * meaningful delivery as a 'deeplink', subsequent ones as 'popstate'.
171
202
  */
172
- deliverQueryParams(params) {
203
+ deliverQueryParams(params, force = false) {
173
204
  const key = this.queryParamsKey(params);
174
- if (key === this._lastDeliveredParamsKey) {
205
+ // The duplicate guard assumes delivered params == applied state, but a
206
+ // component's state can DRIFT after a delivery (internal navigation
207
+ // whose write-back was suppressed or lost). Explicit restores (origin
208
+ // crumb) pass force to reach OnQueryParamsChanged regardless.
209
+ if (key === this._lastDeliveredParamsKey && !force) {
175
210
  return; // Already delivered these exact params.
176
211
  }
177
212
  const isInitial = this._lastDeliveredParamsKey === null;
178
213
  // Don't fire an initial no-op: a component entered without deep-link params has
179
214
  // nothing to apply. Leave _lastDeliveredParamsKey null so the first real params
180
215
  // (whenever they arrive) are still treated as the deep-link entry.
181
- if (isInitial && Object.keys(params).length === 0) {
216
+ // FORCED empties bypass this too: an explicit reset (SwitchToAppHome
217
+ // returning a drifted dashboard to its landing) must reach
218
+ // OnQueryParamsChanged even when nothing was ever delivered.
219
+ if (isInitial && Object.keys(params).length === 0 && !force) {
182
220
  return;
183
221
  }
184
222
  this._lastDeliveredParamsKey = key;
@@ -210,7 +248,22 @@ export class BaseResourceComponent extends BaseNavigationComponent {
210
248
  * wrappers for child dashboards), then falls back to Data.Configuration.tabId.
211
249
  */
212
250
  getTabId() {
213
- return this.ParentTabId || this.Data?.Configuration?.['tabId'] || '';
251
+ return this.ParentTabId || this.reboundTabId || this.Data?.Configuration?.['tabId'] || '';
252
+ }
253
+ /**
254
+ * Re-home this component to a different tab (cache reattach across tab
255
+ * ids). Replaces the reactive param subscription with one bound to the
256
+ * new tab and resets the duplicate-delivery key so the new tab's
257
+ * CURRENT params apply as a fresh mount would — the replay-on-subscribe
258
+ * delivers them immediately.
259
+ */
260
+ RebindTabId(tabId) {
261
+ if (this.getTabId() === tabId) {
262
+ return;
263
+ }
264
+ this.reboundTabId = tabId;
265
+ this._lastDeliveredParamsKey = null;
266
+ this.setupInitialParamDelivery();
214
267
  }
215
268
  getQueryParamUpdateGuard() {
216
269
  const config = this.Data?.Configuration || {};
@@ -261,6 +314,7 @@ export class BaseResourceComponent extends BaseNavigationComponent {
261
314
  * any LATER async state change that must reach the DOM outside those signals.
262
315
  */
263
316
  RefreshView() {
317
+ // Null on lightweight non-view instances (background title resolution)
264
318
  this.changeDetectorRef?.markForCheck();
265
319
  }
266
320
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"base-resource-component.js","sourceRoot":"","sources":["../../src/lib/base-resource-component.ts"],"names":[],"mappings":"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","sourcesContent":["import { ChangeDetectorRef, Directive, OnInit, OnDestroy, Input, inject } from \"@angular/core\";\nimport { Subject } from \"rxjs\";\nimport { filter, takeUntil } from \"rxjs/operators\";\nimport { BaseEntity } from \"@memberjunction/core\";\nimport { BaseNavigationComponent } from \"./base-navigation-component\";\nimport { ResourceData } from \"@memberjunction/core-entities\";\nimport { NavigationService, TabQueryParamUpdateGuard } from \"./navigation.service\";\n\n@Directive()\nexport abstract class BaseResourceComponent extends BaseNavigationComponent implements OnInit, OnDestroy {\n private _data: ResourceData = new ResourceData();\n private _suppressQueryParamSync = false;\n /** Stable key of the most recently delivered query params, used to de-duplicate\n * delivery across the reactive (initial/deep-link) and popstate paths. */\n private _lastDeliveredParamsKey: string | null = null;\n protected destroy$ = new Subject<void>();\n protected navigationService = inject(NavigationService);\n private changeDetectorRef = inject(ChangeDetectorRef, { optional: true });\n\n /**\n * Tab ID for query param notification scoping. Set by resource wrappers\n * that render child dashboards, so the child knows which tab it belongs to.\n * If not set, falls back to Data.Configuration.tabId.\n */\n @Input() ParentTabId: string | null = null;\n\n public get Data(): ResourceData {\n return this._data;\n }\n public set Data(value: ResourceData) {\n this._data = value;\n }\n\n private _loadComplete: boolean = false;\n public get LoadComplete(): boolean {\n return this._loadComplete;\n }\n\n /**\n * Watchdog window: if a resource component hasn't called {@link NotifyLoadComplete} within this\n * time, we log a warning naming the offending class. The app loading screen waits on\n * NotifyLoadComplete, so a resource that never calls it hangs the screen forever — this makes the\n * culprit obvious in the console instead of leaving a mystery \"stuck loading\" state.\n */\n private static readonly LOAD_COMPLETE_WATCHDOG_MS = 15_000;\n private _loadCompleteWatchdog: ReturnType<typeof setTimeout> | null = null;\n\n private _loadStarted: boolean = false;\n public get LoadStarted(): boolean {\n return this._loadStarted;\n }\n\n\n private _loadCompleteEvent: any = null;\n public get LoadCompleteEvent(): any {\n return this._loadCompleteEvent\n }\n public set LoadCompleteEvent(value: any) {\n this._loadCompleteEvent = value;\n }\n\n private _loadStartedEvent: any = null;\n public get LoadStartedEvent(): any {\n return this._loadStartedEvent\n }\n public set LoadStartedEvent(value: any) {\n this._loadStartedEvent = value;\n }\n\n private _resourceRecordSavedEvent: any = null;\n public get ResourceRecordSavedEvent(): any {\n return this._resourceRecordSavedEvent\n }\n public set ResourceRecordSavedEvent(value: any) {\n this._resourceRecordSavedEvent = value;\n }\n\n private _resourceCloseRequestedEvent: (() => void) | null = null;\n public get ResourceCloseRequestedEvent(): (() => void) | null {\n return this._resourceCloseRequestedEvent;\n }\n public set ResourceCloseRequestedEvent(value: (() => void) | null) {\n this._resourceCloseRequestedEvent = value;\n }\n\n private _displayNameChangedEvent: ((newName: string) => void) | null = null;\n public get DisplayNameChangedEvent(): ((newName: string) => void) | null {\n return this._displayNameChangedEvent;\n }\n public set DisplayNameChangedEvent(value: ((newName: string) => void) | null) {\n this._displayNameChangedEvent = value;\n }\n\n ngOnInit(): void {\n // Order matters: set up the popstate subscription first (a plain Subject — emits\n // nothing on subscribe), then the reactive initial-delivery subscription (backed by\n // a BehaviorSubject — emits the current params synchronously). This lets the initial\n // emission claim the 'deeplink' source before any later popstate event.\n this.setupQueryParamSubscription();\n this.setupInitialParamDelivery();\n\n // Start the NotifyLoadComplete watchdog (no-op once it fires or load completes).\n this._loadCompleteWatchdog = setTimeout(() => {\n if (!this._loadComplete) {\n // eslint-disable-next-line no-console\n console.warn(\n `[LoadComplete WATCHDOG] ${this.constructor.name} has NOT called NotifyLoadComplete() ` +\n `within ${BaseResourceComponent.LOAD_COMPLETE_WATCHDOG_MS / 1000}s. The app loading screen ` +\n `waits on this signal — if the screen is stuck, this resource is the likely cause. ` +\n `(tabId=${this.getTabId() || 'n/a'})`\n );\n }\n }, BaseResourceComponent.LOAD_COMPLETE_WATCHDOG_MS);\n }\n\n ngOnDestroy(): void {\n if (this._loadCompleteWatchdog) {\n clearTimeout(this._loadCompleteWatchdog);\n this._loadCompleteWatchdog = null;\n }\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n /**\n * Called by the framework when query params change from an external source\n * (browser back/forward, deep link navigation).\n * Override in subclasses to react to query param changes.\n * @param params The new query params from the URL\n * @param source 'popstate' for back/forward, 'deeplink' for external URL entry\n */\n protected OnQueryParamsChanged(params: Record<string, string>, source: 'popstate' | 'deeplink'): void {\n // Default no-op — override in subclasses\n }\n\n /**\n * Push query param changes to the URL. Creates a browser history entry.\n * Safe to call during OnQueryParamsChanged — auto-suppressed to prevent loops.\n */\n protected UpdateQueryParams(params: Record<string, string | null>): void {\n if (this._suppressQueryParamSync) return;\n const tabId = this.getTabId();\n if (!tabId) {\n this.navigationService.UpdateActiveTabQueryParams(params);\n return;\n }\n\n this.navigationService.UpdateTabQueryParams(tabId, params, this.getQueryParamUpdateGuard());\n }\n\n /**\n * Read current query params from tab configuration.\n * Use in initDashboard() / ngOnInit() to get initial URL state.\n */\n protected GetQueryParams(): Record<string, string> {\n return (this.Data?.Configuration?.['queryParams'] as Record<string, string>) || {};\n }\n\n /**\n * Internal: subscribe to NavigationService query param notifications.\n * Filters to only this component's tab to prevent cross-tab leakage.\n * This is the explicit back/forward (popstate) path.\n */\n private setupQueryParamSubscription(): void {\n this.navigationService.QueryParamChanged$\n .pipe(\n filter(event => {\n const myTabId = this.getTabId();\n return !myTabId || event.TabId === myTabId;\n }),\n takeUntil(this.destroy$)\n )\n .subscribe(event => this.deliverQueryParams(event.Params));\n }\n\n /**\n * Internal: reactively deliver this tab's query params from the workspace\n * BehaviorSubject. Because the source replays the current value on subscribe and\n * emits future changes, this delivers initial deep-link params even when the\n * component mounts (e.g. from workspace restoration) before the URL params have\n * been merged into the tab configuration — closing the cold-load race that the\n * popstate-only path could not.\n */\n private setupInitialParamDelivery(): void {\n const tabId = this.getTabId();\n if (!tabId) {\n return; // No tab scope (e.g. embedded usage) — nothing to observe.\n }\n this.navigationService.ObserveTabQueryParams(tabId)\n .pipe(takeUntil(this.destroy$))\n .subscribe(params => this.deliverQueryParams(params));\n }\n\n /**\n * Internal: funnel point for both the reactive and popstate paths. De-duplicates\n * identical deliveries (the two paths overlap on back/forward) and labels the first\n * meaningful delivery as a 'deeplink', subsequent ones as 'popstate'.\n */\n private deliverQueryParams(params: Record<string, string>): void {\n const key = this.queryParamsKey(params);\n if (key === this._lastDeliveredParamsKey) {\n return; // Already delivered these exact params.\n }\n const isInitial = this._lastDeliveredParamsKey === null;\n // Don't fire an initial no-op: a component entered without deep-link params has\n // nothing to apply. Leave _lastDeliveredParamsKey null so the first real params\n // (whenever they arrive) are still treated as the deep-link entry.\n if (isInitial && Object.keys(params).length === 0) {\n return;\n }\n this._lastDeliveredParamsKey = key;\n const source: 'popstate' | 'deeplink' = isInitial ? 'deeplink' : 'popstate';\n // try/finally ensures the suppression flag is always cleared, and the view is always\n // refreshed, even if OnQueryParamsChanged throws.\n this._suppressQueryParamSync = true;\n try {\n this.OnQueryParamsChanged(params, source);\n } finally {\n this._suppressQueryParamSync = false;\n // Query params arrive via RxJS (workspace stream / popstate), not via a template event —\n // state a subclass changed in OnQueryParamsChanged needs an explicit re-render request\n // for dynamically-attached views (see RefreshView). Inside `finally` so a subclass\n // override that throws still renders whatever partial state it applied before throwing.\n this.RefreshView();\n }\n }\n\n /** Stable, order-independent string key for a query param record. */\n private queryParamsKey(params: Record<string, string>): string {\n return Object.keys(params)\n .sort()\n .map(k => `${k}=${params[k]}`)\n .join('&');\n }\n\n /**\n * Get this component's tab ID. Checks ParentTabId input first (set by resource\n * wrappers for child dashboards), then falls back to Data.Configuration.tabId.\n */\n public getTabId(): string {\n return this.ParentTabId || this.Data?.Configuration?.['tabId'] as string || '';\n }\n\n private getQueryParamUpdateGuard(): TabQueryParamUpdateGuard {\n const config = this.Data?.Configuration || {};\n return {\n resourceType: config['resourceType'] as string | undefined,\n driverClass: (config['resourceTypeDriverClass'] || config['driverClass']) as string | undefined,\n recordId: (this.Data?.ResourceRecordID || config['recordId']) as string | undefined,\n navItemName: config['navItemName'] as string | undefined,\n entity: (config['Entity'] || config['entity']) as string | undefined\n };\n }\n\n protected NotifyLoadComplete() {\n this._loadComplete = true;\n if (this._loadCompleteWatchdog) {\n clearTimeout(this._loadCompleteWatchdog);\n this._loadCompleteWatchdog = null;\n }\n if (this._loadCompleteEvent) {\n this._loadCompleteEvent();\n }\n // Load completion almost always follows async work (RunView/fetch continuations) that\n // Angular's tick will NOT pick up on its own for dynamically-attached views — re-render\n // must be requested explicitly. See RefreshView() for the full explanation.\n this.RefreshView();\n }\n\n protected NotifyLoadStarted() {\n this._loadStarted = true;\n if (this._loadStartedEvent) {\n this._loadStartedEvent();\n }\n this.RefreshView();\n }\n\n /**\n * Marks this component's view (and its ancestors) as needing change detection and schedules\n * a tick — the reliable way for a resource component to re-render after updating state from\n * async work (RunView/RunQuery continuations, timers, websocket pushes, RxJS subscriptions).\n *\n * WHY THIS EXISTS: the Explorer shell hosts resource components dynamically — `createComponent()`\n * + `ApplicationRef.attachView()` — because tab content mounts into Golden Layout / cached DOM\n * containers, not into an Angular template. Since Angular 18's change-detection scheduler\n * rework (Explorer is on Angular 21), `ApplicationRef.tick()` only refreshes attached views\n * that are FLAGGED dirty; a root-level attached view whose component mutates plain fields from\n * an async continuation is never flagged, so the view silently never re-renders — even though\n * the component uses default (non-OnPush) change detection (issue #3106).\n *\n * The framework calls this automatically from NotifyLoadStarted() / NotifyLoadComplete(), so\n * the standard load lifecycle re-renders without any subclass action. Call it yourself after\n * any LATER async state change that must reach the DOM outside those signals.\n */\n protected RefreshView(): void {\n this.changeDetectorRef?.markForCheck();\n }\n\n\n\n /**\n * Call this to notify the tab system that the resource's display name has changed.\n * The tab container will update the tab title and browser title accordingly.\n */\n protected NotifyDisplayNameChanged(newName: string): void {\n if (this._displayNameChangedEvent) {\n this._displayNameChangedEvent(newName);\n }\n }\n\n protected ResourceRecordSaved(resourceRecordEntity: BaseEntity) {\n // Use ToURLSegment, NOT ToString. ResourceRecordID is consumed by\n // EntityRecordResource.GetPrimaryKey → CompositeKey.LoadFromURLSegment, which\n // expects URL-segment format (e.g. \"ID|<uuid>\"). ToString produces \"ID=<uuid>\",\n // which LoadFromURLSegment treats as a raw value and double-prefixes —\n // breaking any code path that re-reads this field after save.\n this.Data.ResourceRecordID = resourceRecordEntity.PrimaryKey.ToURLSegment();\n if (this._resourceRecordSavedEvent) {\n this._resourceRecordSavedEvent(resourceRecordEntity);\n }\n }\n\n /**\n * Ask the host shell to close/dismiss this resource (typically: close the tab).\n * Called by subclasses that hosted a form that emitted a 'dismiss' navigation\n * event — most often, a brand-new record where the user clicked Discard. The\n * record was never saved, the form is empty, and leaving it open serves no\n * purpose. The tab-container listens for this and closes the workspace tab.\n */\n protected NotifyCloseRequested(): void {\n if (this._resourceCloseRequestedEvent) {\n this._resourceCloseRequestedEvent();\n }\n }\n\n abstract GetResourceDisplayName(data: ResourceData): Promise<string>\n\n abstract GetResourceIconClass(data: ResourceData): Promise<string>\n}\n"]}
1
+ {"version":3,"file":"base-resource-component.js","sourceRoot":"","sources":["../../src/lib/base-resource-component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAqB,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC/F,OAAO,EAAE,OAAO,EAAgB,MAAM,MAAM,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAA4B,MAAM,sBAAsB,CAAC;;AAGnF,MAAM,OAAgB,qBAAsB,SAAQ,uBAAuB;IAC/D,KAAK,GAAiB,IAAI,YAAY,EAAE,CAAC;IACzC,uBAAuB,GAAG,KAAK,CAAC;IACxC;+EAC2E;IACnE,uBAAuB,GAAkB,IAAI,CAAC;IAC5C,QAAQ,GAAG,IAAI,OAAO,EAAQ,CAAC;IAC/B,iBAAiB,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC;IACxD;;;;;;;OAOG;IACK,iBAAiB,GAAG,MAAM,CAAC,iBAAiB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAE1E;;;;OAIG;IACM,WAAW,GAAkB,IAAI,CAAC;IAE3C;;;;;;;OAOG;IACK,YAAY,GAAkB,IAAI,CAAC;IAE3C,+EAA+E;IACvE,gBAAgB,GAAwB,IAAI,CAAC;IAErD,IAAW,IAAI;QACX,OAAO,IAAI,CAAC,KAAK,CAAC;IACtB,CAAC;IACD,IAAW,IAAI,CAAC,KAAmB;QAC/B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;IAEO,aAAa,GAAY,KAAK,CAAC;IACvC,IAAW,YAAY;QACnB,OAAO,IAAI,CAAC,aAAa,CAAC;IAC9B,CAAC;IAED;;;;;;;;OAQG;IACK,MAAM,CAAU,yBAAyB,GAAG,MAAM,CAAC;IACnD,qBAAqB,GAAyC,IAAI,CAAC;IAEnE,YAAY,GAAY,KAAK,CAAC;IACtC,IAAW,WAAW;QAClB,OAAO,IAAI,CAAC,YAAY,CAAC;IAC7B,CAAC;IAGO,kBAAkB,GAAQ,IAAI,CAAC;IACvC,IAAW,iBAAiB;QACxB,OAAO,IAAI,CAAC,kBAAkB,CAAA;IAClC,CAAC;IACD,IAAW,iBAAiB,CAAC,KAAU;QACnC,IAAI,CAAC,kBAAkB,GAAG,KAAK,CAAC;IACpC,CAAC;IAEO,iBAAiB,GAAQ,IAAI,CAAC;IACtC,IAAW,gBAAgB;QACvB,OAAO,IAAI,CAAC,iBAAiB,CAAA;IACjC,CAAC;IACD,IAAW,gBAAgB,CAAC,KAAU;QAClC,IAAI,CAAC,iBAAiB,GAAG,KAAK,CAAC;IACnC,CAAC;IAEO,yBAAyB,GAAQ,IAAI,CAAC;IAC9C,IAAW,wBAAwB;QAC/B,OAAO,IAAI,CAAC,yBAAyB,CAAA;IACzC,CAAC;IACD,IAAW,wBAAwB,CAAC,KAAU;QAC1C,IAAI,CAAC,yBAAyB,GAAG,KAAK,CAAC;IAC3C,CAAC;IAEO,4BAA4B,GAAwB,IAAI,CAAC;IACjE,IAAW,2BAA2B;QAClC,OAAO,IAAI,CAAC,4BAA4B,CAAC;IAC7C,CAAC;IACD,IAAW,2BAA2B,CAAC,KAA0B;QAC7D,IAAI,CAAC,4BAA4B,GAAG,KAAK,CAAC;IAC9C,CAAC;IAEO,wBAAwB,GAAuC,IAAI,CAAC;IAC5E,IAAW,uBAAuB;QAC9B,OAAO,IAAI,CAAC,wBAAwB,CAAC;IACzC,CAAC;IACD,IAAW,uBAAuB,CAAC,KAAyC;QACxE,IAAI,CAAC,wBAAwB,GAAG,KAAK,CAAC;IAC1C,CAAC;IAED,QAAQ;QACJ,iFAAiF;QACjF,oFAAoF;QACpF,qFAAqF;QACrF,wEAAwE;QACxE,IAAI,CAAC,2BAA2B,EAAE,CAAC;QACnC,IAAI,CAAC,yBAAyB,EAAE,CAAC;QAEjC,iFAAiF;QACjF,IAAI,CAAC,qBAAqB,GAAG,UAAU,CAAC,GAAG,EAAE;YACzC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;gBACtB,sCAAsC;gBACtC,OAAO,CAAC,IAAI,CACR,2BAA2B,IAAI,CAAC,WAAW,CAAC,IAAI,uCAAuC;oBACvF,UAAU,qBAAqB,CAAC,yBAAyB,GAAG,IAAI,4BAA4B;oBAC5F,6FAA6F;oBAC7F,6FAA6F;oBAC7F,yEAAyE;oBACzE,UAAU,IAAI,CAAC,QAAQ,EAAE,IAAI,KAAK,GAAG,CACxC,CAAC;gBACF,sFAAsF;gBACtF,sFAAsF;gBACtF,sFAAsF;gBACtF,qFAAqF;gBACrF,iFAAiF;gBACjF,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC9B,CAAC;QACL,CAAC,EAAE,qBAAqB,CAAC,yBAAyB,CAAC,CAAC;IACxD,CAAC;IAED,WAAW;QACP,IAAI,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAC7B,YAAY,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;YACzC,IAAI,CAAC,qBAAqB,GAAG,IAAI,CAAC;QACtC,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC;IAC7B,CAAC;IAED;;;;;;OAMG;IACO,oBAAoB,CAAC,MAA8B,EAAE,MAA+B;QAC1F,yCAAyC;IAC7C,CAAC;IAED;;;OAGG;IACO,iBAAiB,CAAC,MAAqC;QAC7D,IAAI,IAAI,CAAC,uBAAuB;YAAE,OAAO;QACzC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC9B,IAAI,CAAC,KAAK,EAAE,CAAC;YACT,IAAI,CAAC,iBAAiB,CAAC,0BAA0B,CAAC,MAAM,CAAC,CAAC;YAC1D,OAAO;QACX,CAAC;QAED,IAAI,CAAC,iBAAiB,CAAC,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,wBAAwB,EAAE,CAAC,CAAC;IAChG,CAAC;IAED;;;OAGG;IACO,cAAc;QACpB,OAAQ,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,CAAC,aAAa,CAA4B,IAAI,EAAE,CAAC;IACvF,CAAC;IAED;;;;OAIG;IACK,2BAA2B;QAC/B,IAAI,CAAC,iBAAiB,CAAC,kBAAkB;aACpC,IAAI,CACD,MAAM,CAAC,KAAK,CAAC,EAAE;YACX,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChC,OAAO,CAAC,OAAO,IAAI,KAAK,CAAC,KAAK,KAAK,OAAO,CAAC;QAC/C,CAAC,CAAC,EACF,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAC3B;aACA,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC;IACzF,CAAC;IAED;;;;;;;OAOG;IACK,yBAAyB;QAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC9B,IAAI,CAAC,KAAK,EAAE,CAAC;YACT,OAAO,CAAC,2DAA2D;QACvE,CAAC;QACD,IAAI,CAAC,gBAAgB,EAAE,WAAW,EAAE,CAAC;QACrC,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,iBAAiB,CAAC,qBAAqB,CAAC,KAAK,CAAC;aACtE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;aAC9B,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED;;;;OAIG;IACK,kBAAkB,CAAC,MAA8B,EAAE,KAAK,GAAG,KAAK;QACpE,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QACxC,uEAAuE;QACvE,oEAAoE;QACpE,sEAAsE;QACtE,8DAA8D;QAC9D,IAAI,GAAG,KAAK,IAAI,CAAC,uBAAuB,IAAI,CAAC,KAAK,EAAE,CAAC;YACjD,OAAO,CAAC,wCAAwC;QACpD,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,uBAAuB,KAAK,IAAI,CAAC;QACxD,gFAAgF;QAChF,gFAAgF;QAChF,mEAAmE;QACnE,qEAAqE;QACrE,2DAA2D;QAC3D,6DAA6D;QAC7D,IAAI,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAC1D,OAAO;QACX,CAAC;QACD,IAAI,CAAC,uBAAuB,GAAG,GAAG,CAAC;QACnC,MAAM,MAAM,GAA4B,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;QAC5E,qFAAqF;QACrF,kDAAkD;QAClD,IAAI,CAAC,uBAAuB,GAAG,IAAI,CAAC;QACpC,IAAI,CAAC;YACD,IAAI,CAAC,oBAAoB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC9C,CAAC;gBAAS,CAAC;YACP,IAAI,CAAC,uBAAuB,GAAG,KAAK,CAAC;YACrC,yFAAyF;YACzF,uFAAuF;YACvF,mFAAmF;YACnF,wFAAwF;YACxF,IAAI,CAAC,WAAW,EAAE,CAAC;QACvB,CAAC;IACL,CAAC;IAED,qEAAqE;IAC7D,cAAc,CAAC,MAA8B;QACjD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;aACrB,IAAI,EAAE;aACN,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;aAC7B,IAAI,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;IAED;;;OAGG;IACI,QAAQ;QACX,OAAO,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,CAAC,OAAO,CAAW,IAAI,EAAE,CAAC;IACxG,CAAC;IAED;;;;;;OAMG;IACI,WAAW,CAAC,KAAa;QAC5B,IAAI,IAAI,CAAC,QAAQ,EAAE,KAAK,KAAK,EAAE,CAAC;YAC5B,OAAO;QACX,CAAC;QACD,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC1B,IAAI,CAAC,uBAAuB,GAAG,IAAI,CAAC;QACpC,IAAI,CAAC,yBAAyB,EAAE,CAAC;IACrC,CAAC;IAEO,wBAAwB;QAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,aAAa,IAAI,EAAE,CAAC;QAC9C,OAAO;YACH,YAAY,EAAE,MAAM,CAAC,cAAc,CAAuB;YAC1D,WAAW,EAAE,CAAC,MAAM,CAAC,yBAAyB,CAAC,IAAI,MAAM,CAAC,aAAa,CAAC,CAAuB;YAC/F,QAAQ,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,IAAI,MAAM,CAAC,UAAU,CAAC,CAAuB;YACnF,WAAW,EAAE,MAAM,CAAC,aAAa,CAAuB;YACxD,MAAM,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAuB;SACvE,CAAC;IACN,CAAC;IAES,kBAAkB;QACxB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC1B,IAAI,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAC7B,YAAY,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;YACzC,IAAI,CAAC,qBAAqB,GAAG,IAAI,CAAC;QACtC,CAAC;QACD,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC1B,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC9B,CAAC;QACD,sFAAsF;QACtF,wFAAwF;QACxF,4EAA4E;QAC5E,IAAI,CAAC,WAAW,EAAE,CAAC;IACvB,CAAC;IAES,iBAAiB;QACvB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QACzB,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACzB,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC7B,CAAC;QACD,IAAI,CAAC,WAAW,EAAE,CAAC;IACvB,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACO,WAAW;QACjB,uEAAuE;QACvE,IAAI,CAAC,iBAAiB,EAAE,YAAY,EAAE,CAAC;IAC3C,CAAC;IAID;;;OAGG;IACO,wBAAwB,CAAC,OAAe;QAC9C,IAAI,IAAI,CAAC,wBAAwB,EAAE,CAAC;YAChC,IAAI,CAAC,wBAAwB,CAAC,OAAO,CAAC,CAAC;QAC3C,CAAC;IACL,CAAC;IAES,mBAAmB,CAAC,oBAAgC;QAC1D,kEAAkE;QAClE,8EAA8E;QAC9E,gFAAgF;QAChF,uEAAuE;QACvE,8DAA8D;QAC9D,IAAI,CAAC,IAAI,CAAC,gBAAgB,GAAG,oBAAoB,CAAC,UAAU,CAAC,YAAY,EAAE,CAAC;QAC5E,IAAI,IAAI,CAAC,yBAAyB,EAAE,CAAC;YACjC,IAAI,CAAC,yBAAyB,CAAC,oBAAoB,CAAC,CAAC;QACzD,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACO,oBAAoB;QAC1B,IAAI,IAAI,CAAC,4BAA4B,EAAE,CAAC;YACpC,IAAI,CAAC,4BAA4B,EAAE,CAAC;QACxC,CAAC;IACL,CAAC;iQA7XiB,qBAAqB,yBAArB,qBAAqB;6DAArB,qBAAqB;;iFAArB,qBAAqB;cAD1C,SAAS;;kBAwBL,KAAK","sourcesContent":["import { ChangeDetectorRef, Directive, OnInit, OnDestroy, Input, inject } from \"@angular/core\";\nimport { Subject, Subscription } from \"rxjs\";\nimport { filter, takeUntil } from \"rxjs/operators\";\nimport { BaseEntity } from \"@memberjunction/core\";\nimport { BaseNavigationComponent } from \"./base-navigation-component\";\nimport { ResourceData } from \"@memberjunction/core-entities\";\nimport { NavigationService, TabQueryParamUpdateGuard } from \"./navigation.service\";\n\n@Directive()\nexport abstract class BaseResourceComponent extends BaseNavigationComponent implements OnInit, OnDestroy {\n private _data: ResourceData = new ResourceData();\n private _suppressQueryParamSync = false;\n /** Stable key of the most recently delivered query params, used to de-duplicate\n * delivery across the reactive (initial/deep-link) and popstate paths. */\n private _lastDeliveredParamsKey: string | null = null;\n protected destroy$ = new Subject<void>();\n protected navigationService = inject(NavigationService);\n /**\n * Optional: the tab container instantiates LIGHTWEIGHT throwaway instances\n * of resource components (via runInInjectionContext with an environment\n * injector) purely to call GetResourceDisplayName for background tab\n * titles. ChangeDetectorRef only exists in node injectors, so a required\n * inject() there throws NG0201 and silently kills every background title\n * resolution — tabs keep raw \"Entity - ID|guid\" titles until opened.\n */\n private changeDetectorRef = inject(ChangeDetectorRef, { optional: true });\n\n /**\n * Tab ID for query param notification scoping. Set by resource wrappers\n * that render child dashboards, so the child knows which tab it belongs to.\n * If not set, falls back to Data.Configuration.tabId.\n */\n @Input() ParentTabId: string | null = null;\n\n /**\n * Tab id this component was RE-HOMED to by a cache reattach. The\n * component cache keys on driver+record+app — NOT tab id — so a cached\n * component can be reattached to a different tab than it was born under.\n * Without rebinding, its query-param subscription listens to the DEAD\n * birth tab forever and every delivery to the live tab is lost (the\n * \"crumb lands on the dashboard root after promote/demote\" bug).\n */\n private reboundTabId: string | null = null;\n\n /** Handle for the reactive param subscription so RebindTabId can replace it */\n private reactiveParamSub: Subscription | null = null;\n\n public get Data(): ResourceData {\n return this._data;\n }\n public set Data(value: ResourceData) {\n this._data = value;\n }\n\n private _loadComplete: boolean = false;\n public get LoadComplete(): boolean {\n return this._loadComplete;\n }\n\n /**\n * Watchdog window: if a resource component hasn't called {@link NotifyLoadComplete} within this\n * time, we log a warning naming the offending class AND fail open — forcing NotifyLoadComplete\n * so the app loading screen (which blocks on that signal) is released. Without the fail-open, a\n * resource that never signals — because it errored, hung, or has its own `ngOnInit` that\n * bypasses BaseDashboard's guarded lifecycle — would hang the whole Explorer forever. This is\n * the safety net covering EVERY BaseResourceComponent subclass, not just BaseDashboard (whose\n * own try/finally already guarantees the signal on the normal path).\n */\n private static readonly LOAD_COMPLETE_WATCHDOG_MS = 15_000;\n private _loadCompleteWatchdog: ReturnType<typeof setTimeout> | null = null;\n\n private _loadStarted: boolean = false;\n public get LoadStarted(): boolean {\n return this._loadStarted;\n }\n\n\n private _loadCompleteEvent: any = null;\n public get LoadCompleteEvent(): any {\n return this._loadCompleteEvent\n }\n public set LoadCompleteEvent(value: any) {\n this._loadCompleteEvent = value;\n }\n\n private _loadStartedEvent: any = null;\n public get LoadStartedEvent(): any {\n return this._loadStartedEvent\n }\n public set LoadStartedEvent(value: any) {\n this._loadStartedEvent = value;\n }\n\n private _resourceRecordSavedEvent: any = null;\n public get ResourceRecordSavedEvent(): any {\n return this._resourceRecordSavedEvent\n }\n public set ResourceRecordSavedEvent(value: any) {\n this._resourceRecordSavedEvent = value;\n }\n\n private _resourceCloseRequestedEvent: (() => void) | null = null;\n public get ResourceCloseRequestedEvent(): (() => void) | null {\n return this._resourceCloseRequestedEvent;\n }\n public set ResourceCloseRequestedEvent(value: (() => void) | null) {\n this._resourceCloseRequestedEvent = value;\n }\n\n private _displayNameChangedEvent: ((newName: string) => void) | null = null;\n public get DisplayNameChangedEvent(): ((newName: string) => void) | null {\n return this._displayNameChangedEvent;\n }\n public set DisplayNameChangedEvent(value: ((newName: string) => void) | null) {\n this._displayNameChangedEvent = value;\n }\n\n ngOnInit(): void {\n // Order matters: set up the popstate subscription first (a plain Subject — emits\n // nothing on subscribe), then the reactive initial-delivery subscription (backed by\n // a BehaviorSubject — emits the current params synchronously). This lets the initial\n // emission claim the 'deeplink' source before any later popstate event.\n this.setupQueryParamSubscription();\n this.setupInitialParamDelivery();\n\n // Start the NotifyLoadComplete watchdog (no-op once it fires or load completes).\n this._loadCompleteWatchdog = setTimeout(() => {\n if (!this._loadComplete) {\n // eslint-disable-next-line no-console\n console.warn(\n `[LoadComplete WATCHDOG] ${this.constructor.name} has NOT called NotifyLoadComplete() ` +\n `within ${BaseResourceComponent.LOAD_COMPLETE_WATCHDOG_MS / 1000}s — FAILING OPEN: forcing ` +\n `load-complete to release the app loading screen. This resource either errored/hung without ` +\n `signalling, or is simply still loading (a slow-but-healthy load re-signals harmlessly when ` +\n `it finishes). If you are chasing a \"stuck loading\" report, start here. ` +\n `(tabId=${this.getTabId() || 'n/a'})`\n );\n // Fail-open: the shell's loading screen blocks on NotifyLoadComplete. A subclass that\n // never calls it — one whose own ngOnInit bypasses BaseDashboard's guarded lifecycle,\n // or whose load genuinely hangs — would otherwise brick the whole Explorer. Force the\n // signal so no single resource can take down the shell. (Idempotent-safe: NotifyLoad\n // Complete is already called repeatedly on the normal path, e.g. every Refresh.)\n this.NotifyLoadComplete();\n }\n }, BaseResourceComponent.LOAD_COMPLETE_WATCHDOG_MS);\n }\n\n ngOnDestroy(): void {\n if (this._loadCompleteWatchdog) {\n clearTimeout(this._loadCompleteWatchdog);\n this._loadCompleteWatchdog = null;\n }\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n /**\n * Called by the framework when query params change from an external source\n * (browser back/forward, deep link navigation).\n * Override in subclasses to react to query param changes.\n * @param params The new query params from the URL\n * @param source 'popstate' for back/forward, 'deeplink' for external URL entry\n */\n protected OnQueryParamsChanged(params: Record<string, string>, source: 'popstate' | 'deeplink'): void {\n // Default no-op — override in subclasses\n }\n\n /**\n * Push query param changes to the URL. Creates a browser history entry.\n * Safe to call during OnQueryParamsChanged — auto-suppressed to prevent loops.\n */\n protected UpdateQueryParams(params: Record<string, string | null>): void {\n if (this._suppressQueryParamSync) return;\n const tabId = this.getTabId();\n if (!tabId) {\n this.navigationService.UpdateActiveTabQueryParams(params);\n return;\n }\n\n this.navigationService.UpdateTabQueryParams(tabId, params, this.getQueryParamUpdateGuard());\n }\n\n /**\n * Read current query params from tab configuration.\n * Use in initDashboard() / ngOnInit() to get initial URL state.\n */\n protected GetQueryParams(): Record<string, string> {\n return (this.Data?.Configuration?.['queryParams'] as Record<string, string>) || {};\n }\n\n /**\n * Internal: subscribe to NavigationService query param notifications.\n * Filters to only this component's tab to prevent cross-tab leakage.\n * This is the explicit back/forward (popstate) path.\n */\n private setupQueryParamSubscription(): void {\n this.navigationService.QueryParamChanged$\n .pipe(\n filter(event => {\n const myTabId = this.getTabId();\n return !myTabId || event.TabId === myTabId;\n }),\n takeUntil(this.destroy$)\n )\n .subscribe(event => this.deliverQueryParams(event.Params, event.Force === true));\n }\n\n /**\n * Internal: reactively deliver this tab's query params from the workspace\n * BehaviorSubject. Because the source replays the current value on subscribe and\n * emits future changes, this delivers initial deep-link params even when the\n * component mounts (e.g. from workspace restoration) before the URL params have\n * been merged into the tab configuration — closing the cold-load race that the\n * popstate-only path could not.\n */\n private setupInitialParamDelivery(): void {\n const tabId = this.getTabId();\n if (!tabId) {\n return; // No tab scope (e.g. embedded usage) — nothing to observe.\n }\n this.reactiveParamSub?.unsubscribe();\n this.reactiveParamSub = this.navigationService.ObserveTabQueryParams(tabId)\n .pipe(takeUntil(this.destroy$))\n .subscribe(params => this.deliverQueryParams(params));\n }\n\n /**\n * Internal: funnel point for both the reactive and popstate paths. De-duplicates\n * identical deliveries (the two paths overlap on back/forward) and labels the first\n * meaningful delivery as a 'deeplink', subsequent ones as 'popstate'.\n */\n private deliverQueryParams(params: Record<string, string>, force = false): void {\n const key = this.queryParamsKey(params);\n // The duplicate guard assumes delivered params == applied state, but a\n // component's state can DRIFT after a delivery (internal navigation\n // whose write-back was suppressed or lost). Explicit restores (origin\n // crumb) pass force to reach OnQueryParamsChanged regardless.\n if (key === this._lastDeliveredParamsKey && !force) {\n return; // Already delivered these exact params.\n }\n const isInitial = this._lastDeliveredParamsKey === null;\n // Don't fire an initial no-op: a component entered without deep-link params has\n // nothing to apply. Leave _lastDeliveredParamsKey null so the first real params\n // (whenever they arrive) are still treated as the deep-link entry.\n // FORCED empties bypass this too: an explicit reset (SwitchToAppHome\n // returning a drifted dashboard to its landing) must reach\n // OnQueryParamsChanged even when nothing was ever delivered.\n if (isInitial && Object.keys(params).length === 0 && !force) {\n return;\n }\n this._lastDeliveredParamsKey = key;\n const source: 'popstate' | 'deeplink' = isInitial ? 'deeplink' : 'popstate';\n // try/finally ensures the suppression flag is always cleared, and the view is always\n // refreshed, even if OnQueryParamsChanged throws.\n this._suppressQueryParamSync = true;\n try {\n this.OnQueryParamsChanged(params, source);\n } finally {\n this._suppressQueryParamSync = false;\n // Query params arrive via RxJS (workspace stream / popstate), not via a template event —\n // state a subclass changed in OnQueryParamsChanged needs an explicit re-render request\n // for dynamically-attached views (see RefreshView). Inside `finally` so a subclass\n // override that throws still renders whatever partial state it applied before throwing.\n this.RefreshView();\n }\n }\n\n /** Stable, order-independent string key for a query param record. */\n private queryParamsKey(params: Record<string, string>): string {\n return Object.keys(params)\n .sort()\n .map(k => `${k}=${params[k]}`)\n .join('&');\n }\n\n /**\n * Get this component's tab ID. Checks ParentTabId input first (set by resource\n * wrappers for child dashboards), then falls back to Data.Configuration.tabId.\n */\n public getTabId(): string {\n return this.ParentTabId || this.reboundTabId || this.Data?.Configuration?.['tabId'] as string || '';\n }\n\n /**\n * Re-home this component to a different tab (cache reattach across tab\n * ids). Replaces the reactive param subscription with one bound to the\n * new tab and resets the duplicate-delivery key so the new tab's\n * CURRENT params apply as a fresh mount would — the replay-on-subscribe\n * delivers them immediately.\n */\n public RebindTabId(tabId: string): void {\n if (this.getTabId() === tabId) {\n return;\n }\n this.reboundTabId = tabId;\n this._lastDeliveredParamsKey = null;\n this.setupInitialParamDelivery();\n }\n\n private getQueryParamUpdateGuard(): TabQueryParamUpdateGuard {\n const config = this.Data?.Configuration || {};\n return {\n resourceType: config['resourceType'] as string | undefined,\n driverClass: (config['resourceTypeDriverClass'] || config['driverClass']) as string | undefined,\n recordId: (this.Data?.ResourceRecordID || config['recordId']) as string | undefined,\n navItemName: config['navItemName'] as string | undefined,\n entity: (config['Entity'] || config['entity']) as string | undefined\n };\n }\n\n protected NotifyLoadComplete() {\n this._loadComplete = true;\n if (this._loadCompleteWatchdog) {\n clearTimeout(this._loadCompleteWatchdog);\n this._loadCompleteWatchdog = null;\n }\n if (this._loadCompleteEvent) {\n this._loadCompleteEvent();\n }\n // Load completion almost always follows async work (RunView/fetch continuations) that\n // Angular's tick will NOT pick up on its own for dynamically-attached views — re-render\n // must be requested explicitly. See RefreshView() for the full explanation.\n this.RefreshView();\n }\n\n protected NotifyLoadStarted() {\n this._loadStarted = true;\n if (this._loadStartedEvent) {\n this._loadStartedEvent();\n }\n this.RefreshView();\n }\n\n /**\n * Marks this component's view (and its ancestors) as needing change detection and schedules\n * a tick — the reliable way for a resource component to re-render after updating state from\n * async work (RunView/RunQuery continuations, timers, websocket pushes, RxJS subscriptions).\n *\n * WHY THIS EXISTS: the Explorer shell hosts resource components dynamically — `createComponent()`\n * + `ApplicationRef.attachView()` — because tab content mounts into Golden Layout / cached DOM\n * containers, not into an Angular template. Since Angular 18's change-detection scheduler\n * rework (Explorer is on Angular 21), `ApplicationRef.tick()` only refreshes attached views\n * that are FLAGGED dirty; a root-level attached view whose component mutates plain fields from\n * an async continuation is never flagged, so the view silently never re-renders — even though\n * the component uses default (non-OnPush) change detection (issue #3106).\n *\n * The framework calls this automatically from NotifyLoadStarted() / NotifyLoadComplete(), so\n * the standard load lifecycle re-renders without any subclass action. Call it yourself after\n * any LATER async state change that must reach the DOM outside those signals.\n */\n protected RefreshView(): void {\n // Null on lightweight non-view instances (background title resolution)\n this.changeDetectorRef?.markForCheck();\n }\n\n\n\n /**\n * Call this to notify the tab system that the resource's display name has changed.\n * The tab container will update the tab title and browser title accordingly.\n */\n protected NotifyDisplayNameChanged(newName: string): void {\n if (this._displayNameChangedEvent) {\n this._displayNameChangedEvent(newName);\n }\n }\n\n protected ResourceRecordSaved(resourceRecordEntity: BaseEntity) {\n // Use ToURLSegment, NOT ToString. ResourceRecordID is consumed by\n // EntityRecordResource.GetPrimaryKey → CompositeKey.LoadFromURLSegment, which\n // expects URL-segment format (e.g. \"ID|<uuid>\"). ToString produces \"ID=<uuid>\",\n // which LoadFromURLSegment treats as a raw value and double-prefixes —\n // breaking any code path that re-reads this field after save.\n this.Data.ResourceRecordID = resourceRecordEntity.PrimaryKey.ToURLSegment();\n if (this._resourceRecordSavedEvent) {\n this._resourceRecordSavedEvent(resourceRecordEntity);\n }\n }\n\n /**\n * Ask the host shell to close/dismiss this resource (typically: close the tab).\n * Called by subclasses that hosted a form that emitted a 'dismiss' navigation\n * event — most often, a brand-new record where the user clicked Discard. The\n * record was never saved, the form is empty, and leaving it open serves no\n * purpose. The tab-container listens for this and closes the workspace tab.\n */\n protected NotifyCloseRequested(): void {\n if (this._resourceCloseRequestedEvent) {\n this._resourceCloseRequestedEvent();\n }\n }\n\n abstract GetResourceDisplayName(data: ResourceData): Promise<string>\n\n abstract GetResourceIconClass(data: ResourceData): Promise<string>\n}\n"]}
@@ -0,0 +1,39 @@
1
+ import { OnDestroy } from '@angular/core';
2
+ import { Observable } from 'rxjs';
3
+ import * as i0 from "@angular/core";
4
+ /**
5
+ * The SHELL mobile chrome breakpoint. Matches the `@media (max-width: 768px)`
6
+ * blocks in shell.component.css that flip the hamburger/drawer chrome on —
7
+ * TS consumers (records mobile mode, the drawer's Records pill) must engage
8
+ * at exactly the same width or the shell shows mobile chrome while the
9
+ * records surface still renders its desktop strip (or vice versa).
10
+ *
11
+ * NOTE: the newer ui-components chrome (left-nav drawer, filter-popover
12
+ * sheet, page-body) breaks at 700px — that's component-local chrome with its
13
+ * own constant. This one is the shell's.
14
+ */
15
+ export declare const EXPLORER_MOBILE_BREAKPOINT_PX = 768;
16
+ /**
17
+ * Reactive "is the shell in mobile chrome?" signal.
18
+ *
19
+ * matchMedia's `change` event fires OUTSIDE the Angular zone; the handler
20
+ * re-enters via zone.run (same pattern as mj-filter-popover) so template
21
+ * flips driven by the emission are seen by change detection. Zoneless
22
+ * subscribers should still call SafeDetectChanges after mutating their own
23
+ * state in the subscription.
24
+ */
25
+ export declare class ExplorerBreakpointService implements OnDestroy {
26
+ private zone;
27
+ private query;
28
+ private isMobile$;
29
+ private onChange;
30
+ constructor();
31
+ /** Current value, synchronous. */
32
+ get IsMobile(): boolean;
33
+ /** Emits the current value on subscribe, then every breakpoint crossing. */
34
+ get IsMobile$(): Observable<boolean>;
35
+ ngOnDestroy(): void;
36
+ static ɵfac: i0.ɵɵFactoryDeclaration<ExplorerBreakpointService, never>;
37
+ static ɵprov: i0.ɵɵInjectableDeclaration<ExplorerBreakpointService>;
38
+ }
39
+ //# sourceMappingURL=explorer-breakpoint.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explorer-breakpoint.service.d.ts","sourceRoot":"","sources":["../../src/lib/explorer-breakpoint.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,SAAS,EAAU,MAAM,eAAe,CAAC;AACtE,OAAO,EAAmB,UAAU,EAAE,MAAM,MAAM,CAAC;;AAEnD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,6BAA6B,MAAM,CAAC;AAEjD;;;;;;;;GAQG;AACH,qBACa,yBAA0B,YAAW,SAAS;IACzD,OAAO,CAAC,IAAI,CAAkB;IAE9B,OAAO,CAAC,KAAK,CAGF;IAEX,OAAO,CAAC,SAAS,CAA8D;IAE/E,OAAO,CAAC,QAAQ,CAEd;;IAMF,kCAAkC;IAClC,IAAW,QAAQ,IAAI,OAAO,CAE7B;IAED,4EAA4E;IAC5E,IAAW,SAAS,IAAI,UAAU,CAAC,OAAO,CAAC,CAE1C;IAID,WAAW,IAAI,IAAI;yCA9BR,yBAAyB;6CAAzB,yBAAyB;CAiCrC"}
@@ -0,0 +1,57 @@
1
+ import { Injectable, NgZone, inject } from '@angular/core';
2
+ import { BehaviorSubject } from 'rxjs';
3
+ import * as i0 from "@angular/core";
4
+ /**
5
+ * The SHELL mobile chrome breakpoint. Matches the `@media (max-width: 768px)`
6
+ * blocks in shell.component.css that flip the hamburger/drawer chrome on —
7
+ * TS consumers (records mobile mode, the drawer's Records pill) must engage
8
+ * at exactly the same width or the shell shows mobile chrome while the
9
+ * records surface still renders its desktop strip (or vice versa).
10
+ *
11
+ * NOTE: the newer ui-components chrome (left-nav drawer, filter-popover
12
+ * sheet, page-body) breaks at 700px — that's component-local chrome with its
13
+ * own constant. This one is the shell's.
14
+ */
15
+ export const EXPLORER_MOBILE_BREAKPOINT_PX = 768;
16
+ /**
17
+ * Reactive "is the shell in mobile chrome?" signal.
18
+ *
19
+ * matchMedia's `change` event fires OUTSIDE the Angular zone; the handler
20
+ * re-enters via zone.run (same pattern as mj-filter-popover) so template
21
+ * flips driven by the emission are seen by change detection. Zoneless
22
+ * subscribers should still call SafeDetectChanges after mutating their own
23
+ * state in the subscription.
24
+ */
25
+ export class ExplorerBreakpointService {
26
+ zone = inject(NgZone);
27
+ query = typeof window !== 'undefined' && typeof window.matchMedia === 'function'
28
+ ? window.matchMedia(`(max-width: ${EXPLORER_MOBILE_BREAKPOINT_PX}px)`)
29
+ : null;
30
+ isMobile$ = new BehaviorSubject(this.query?.matches ?? false);
31
+ onChange = (e) => {
32
+ this.zone.run(() => this.isMobile$.next(e.matches));
33
+ };
34
+ constructor() {
35
+ this.query?.addEventListener('change', this.onChange);
36
+ }
37
+ /** Current value, synchronous. */
38
+ get IsMobile() {
39
+ return this.isMobile$.value;
40
+ }
41
+ /** Emits the current value on subscribe, then every breakpoint crossing. */
42
+ get IsMobile$() {
43
+ return this.isMobile$.asObservable();
44
+ }
45
+ // Root providers are destroyed on ApplicationRef teardown (tests,
46
+ // multi-bootstrap) — release the matchMedia listener with them
47
+ ngOnDestroy() {
48
+ this.query?.removeEventListener('change', this.onChange);
49
+ }
50
+ static ɵfac = function ExplorerBreakpointService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || ExplorerBreakpointService)(); };
51
+ static ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: ExplorerBreakpointService, factory: ExplorerBreakpointService.ɵfac, providedIn: 'root' });
52
+ }
53
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ExplorerBreakpointService, [{
54
+ type: Injectable,
55
+ args: [{ providedIn: 'root' }]
56
+ }], () => [], null); })();
57
+ //# sourceMappingURL=explorer-breakpoint.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explorer-breakpoint.service.js","sourceRoot":"","sources":["../../src/lib/explorer-breakpoint.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAa,MAAM,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,EAAE,eAAe,EAAc,MAAM,MAAM,CAAC;;AAEnD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,GAAG,CAAC;AAEjD;;;;;;;;GAQG;AAEH,MAAM,OAAO,yBAAyB;IAC5B,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAEtB,KAAK,GACX,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU;QACtE,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,eAAe,6BAA6B,KAAK,CAAC;QACtE,CAAC,CAAC,IAAI,CAAC;IAEH,SAAS,GAAG,IAAI,eAAe,CAAU,IAAI,CAAC,KAAK,EAAE,OAAO,IAAI,KAAK,CAAC,CAAC;IAEvE,QAAQ,GAAG,CAAC,CAAsB,EAAQ,EAAE;QAClD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACtD,CAAC,CAAC;IAEF;QACE,IAAI,CAAC,KAAK,EAAE,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxD,CAAC;IAED,kCAAkC;IAClC,IAAW,QAAQ;QACjB,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;IAC9B,CAAC;IAED,4EAA4E;IAC5E,IAAW,SAAS;QAClB,OAAO,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC;IACvC,CAAC;IAED,kEAAkE;IAClE,+DAA+D;IAC/D,WAAW;QACT,IAAI,CAAC,KAAK,EAAE,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC3D,CAAC;mHAhCU,yBAAyB;gEAAzB,yBAAyB,WAAzB,yBAAyB,mBADZ,MAAM;;iFACnB,yBAAyB;cADrC,UAAU;eAAC,EAAE,UAAU,EAAE,MAAM,EAAE","sourcesContent":["import { Injectable, NgZone, OnDestroy, inject } from '@angular/core';\nimport { BehaviorSubject, Observable } from 'rxjs';\n\n/**\n * The SHELL mobile chrome breakpoint. Matches the `@media (max-width: 768px)`\n * blocks in shell.component.css that flip the hamburger/drawer chrome on —\n * TS consumers (records mobile mode, the drawer's Records pill) must engage\n * at exactly the same width or the shell shows mobile chrome while the\n * records surface still renders its desktop strip (or vice versa).\n *\n * NOTE: the newer ui-components chrome (left-nav drawer, filter-popover\n * sheet, page-body) breaks at 700px — that's component-local chrome with its\n * own constant. This one is the shell's.\n */\nexport const EXPLORER_MOBILE_BREAKPOINT_PX = 768;\n\n/**\n * Reactive \"is the shell in mobile chrome?\" signal.\n *\n * matchMedia's `change` event fires OUTSIDE the Angular zone; the handler\n * re-enters via zone.run (same pattern as mj-filter-popover) so template\n * flips driven by the emission are seen by change detection. Zoneless\n * subscribers should still call SafeDetectChanges after mutating their own\n * state in the subscription.\n */\n@Injectable({ providedIn: 'root' })\nexport class ExplorerBreakpointService implements OnDestroy {\n private zone = inject(NgZone);\n\n private query: MediaQueryList | null =\n typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n ? window.matchMedia(`(max-width: ${EXPLORER_MOBILE_BREAKPOINT_PX}px)`)\n : null;\n\n private isMobile$ = new BehaviorSubject<boolean>(this.query?.matches ?? false);\n\n private onChange = (e: MediaQueryListEvent): void => {\n this.zone.run(() => this.isMobile$.next(e.matches));\n };\n\n constructor() {\n this.query?.addEventListener('change', this.onChange);\n }\n\n /** Current value, synchronous. */\n public get IsMobile(): boolean {\n return this.isMobile$.value;\n }\n\n /** Emits the current value on subscribe, then every breakpoint crossing. */\n public get IsMobile$(): Observable<boolean> {\n return this.isMobile$.asObservable();\n }\n\n // Root providers are destroyed on ApplicationRef teardown (tests,\n // multi-bootstrap) — release the matchMedia listener with them\n ngOnDestroy(): void {\n this.query?.removeEventListener('change', this.onChange);\n }\n}\n"]}
@@ -1,7 +1,20 @@
1
+ import { RecordSourceContext } from './record-open-style';
1
2
  /**
2
3
  * Options for controlling navigation behavior
3
4
  */
4
5
  export interface NavigationOptions {
6
+ /**
7
+ * Origin override for record opens (records-style origin crumb).
8
+ * - omitted: NavigationService snapshots the active app/tab — correct for
9
+ * in-tab surfaces, WRONG for overlays/dialogs (it blames the page behind
10
+ * them).
11
+ * - a RecordSourceContext: used verbatim. Overlay surfaces set at least
12
+ * `sourceLabel` ('Conversation', 'Search', ...), plus app/nav-label when
13
+ * there is a real place to return to.
14
+ * - 'none': the open has no meaningful origin (e.g. recreating a closed
15
+ * tab from browser history) — no crumb is rendered.
16
+ */
17
+ recordSource?: RecordSourceContext | 'none';
5
18
  /**
6
19
  * Force creation of a new tab instead of replacing temporary tabs
7
20
  * If not specified, the NavigationService will use global shift-key detection
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.interfaces.d.ts","sourceRoot":"","sources":["../../src/lib/navigation.interfaces.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE1C;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;CAC7C"}
1
+ {"version":3,"file":"navigation.interfaces.d.ts","sourceRoot":"","sources":["../../src/lib/navigation.interfaces.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAAC;IAE5C;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;OAEG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE1C;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;CAC7C"}