@memberjunction/ng-explorer-core 6.2.0-edge.4 → 6.2.0-edge.6

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 (76) hide show
  1. package/README.md +7 -2
  2. package/dist/app-routing.module.d.ts.map +1 -1
  3. package/dist/app-routing.module.js +20 -10
  4. package/dist/app-routing.module.js.map +1 -1
  5. package/dist/generated/lazy-feature-config.d.ts +1 -1
  6. package/dist/generated/lazy-feature-config.d.ts.map +1 -1
  7. package/dist/generated/lazy-feature-config.js +6 -3
  8. package/dist/generated/lazy-feature-config.js.map +1 -1
  9. package/dist/lib/oauth/oauth-callback.component.d.ts +2 -0
  10. package/dist/lib/oauth/oauth-callback.component.d.ts.map +1 -1
  11. package/dist/lib/oauth/oauth-callback.component.js +10 -10
  12. package/dist/lib/oauth/oauth-callback.component.js.map +1 -1
  13. package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts +17 -3
  14. package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts.map +1 -1
  15. package/dist/lib/resource-wrappers/chat-conversations-resource.component.js +211 -13
  16. package/dist/lib/resource-wrappers/chat-conversations-resource.component.js.map +1 -1
  17. package/dist/lib/resource-wrappers/chat-tasks-resource.component.d.ts +12 -3
  18. package/dist/lib/resource-wrappers/chat-tasks-resource.component.d.ts.map +1 -1
  19. package/dist/lib/resource-wrappers/chat-tasks-resource.component.js +18 -3
  20. package/dist/lib/resource-wrappers/chat-tasks-resource.component.js.map +1 -1
  21. package/dist/lib/resource-wrappers/record-form-mode.d.ts +44 -0
  22. package/dist/lib/resource-wrappers/record-form-mode.d.ts.map +1 -0
  23. package/dist/lib/resource-wrappers/record-form-mode.js +47 -0
  24. package/dist/lib/resource-wrappers/record-form-mode.js.map +1 -0
  25. package/dist/lib/resource-wrappers/record-resource.component.d.ts +56 -1
  26. package/dist/lib/resource-wrappers/record-resource.component.d.ts.map +1 -1
  27. package/dist/lib/resource-wrappers/record-resource.component.js +124 -9
  28. package/dist/lib/resource-wrappers/record-resource.component.js.map +1 -1
  29. package/dist/lib/resource-wrappers/search-results-resource.component.d.ts +3 -3
  30. package/dist/lib/resource-wrappers/search-results-resource.component.d.ts.map +1 -1
  31. package/dist/lib/resource-wrappers/search-results-resource.component.js +5 -10
  32. package/dist/lib/resource-wrappers/search-results-resource.component.js.map +1 -1
  33. package/dist/lib/resource-wrappers/standard-form-create.d.ts +4 -0
  34. package/dist/lib/resource-wrappers/standard-form-create.d.ts.map +1 -0
  35. package/dist/lib/resource-wrappers/standard-form-create.js +6 -0
  36. package/dist/lib/resource-wrappers/standard-form-create.js.map +1 -0
  37. package/dist/lib/resource-wrappers/view-resource.component.d.ts +18 -1
  38. package/dist/lib/resource-wrappers/view-resource.component.d.ts.map +1 -1
  39. package/dist/lib/resource-wrappers/view-resource.component.js +105 -39
  40. package/dist/lib/resource-wrappers/view-resource.component.js.map +1 -1
  41. package/dist/lib/server-connectivity/server-connectivity-banner.component.d.ts +16 -3
  42. package/dist/lib/server-connectivity/server-connectivity-banner.component.d.ts.map +1 -1
  43. package/dist/lib/server-connectivity/server-connectivity-banner.component.js +17 -3
  44. package/dist/lib/server-connectivity/server-connectivity-banner.component.js.map +1 -1
  45. package/dist/lib/shell/components/record-open/record-origin-crumb.component.d.ts.map +1 -1
  46. package/dist/lib/shell/components/record-open/record-origin-crumb.component.js +12 -8
  47. package/dist/lib/shell/components/record-open/record-origin-crumb.component.js.map +1 -1
  48. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts +6 -0
  49. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
  50. package/dist/lib/shell/components/tabs/component-cache-manager.js +18 -3
  51. package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
  52. package/dist/lib/shell/components/tabs/tab-container.component.d.ts +36 -0
  53. package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
  54. package/dist/lib/shell/components/tabs/tab-container.component.js +188 -47
  55. package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
  56. package/dist/lib/shell/deep-link-workspace.d.ts +25 -0
  57. package/dist/lib/shell/deep-link-workspace.d.ts.map +1 -0
  58. package/dist/lib/shell/deep-link-workspace.js +49 -0
  59. package/dist/lib/shell/deep-link-workspace.js.map +1 -0
  60. package/dist/lib/shell/shell-chrome-policy.d.ts +6 -0
  61. package/dist/lib/shell/shell-chrome-policy.d.ts.map +1 -1
  62. package/dist/lib/shell/shell-chrome-policy.js +1 -0
  63. package/dist/lib/shell/shell-chrome-policy.js.map +1 -1
  64. package/dist/lib/shell/shell.component.d.ts +35 -0
  65. package/dist/lib/shell/shell.component.d.ts.map +1 -1
  66. package/dist/lib/shell/shell.component.js +348 -78
  67. package/dist/lib/shell/shell.component.js.map +1 -1
  68. package/dist/lib/single-record/single-record.component.d.ts +39 -2
  69. package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
  70. package/dist/lib/single-record/single-record.component.js +99 -12
  71. package/dist/lib/single-record/single-record.component.js.map +1 -1
  72. package/dist/lib/user-notifications/user-notifications.component.d.ts +10 -2
  73. package/dist/lib/user-notifications/user-notifications.component.d.ts.map +1 -1
  74. package/dist/lib/user-notifications/user-notifications.component.js +77 -22
  75. package/dist/lib/user-notifications/user-notifications.component.js.map +1 -1
  76. package/package.json +57 -57
@@ -1,7 +1,7 @@
1
1
  import { Component, ViewChild, runInInjectionContext, createComponent, ViewEncapsulation, HostListener, Output, EventEmitter, inject } from '@angular/core';
2
- import { GoldenLayoutManager, FlattenLayoutToSingleStack } from '@memberjunction/ng-base-application';
2
+ import { GoldenLayoutManager, FlattenLayoutToSingleStack, TabService } from '@memberjunction/ng-base-application';
3
3
  import { MJGlobal } from '@memberjunction/global';
4
- import { BaseResourceComponent, HomeAppPinService, NavigationService, IsRecordTabsStyle, IsRecordsTabConfiguration, IsRecordsRegionTab, IsRecordDockedToWorkspace, RECORD_DOCKED_TO_WORKSPACE_KEY, GetRecordSourceContext, SafeDetectChanges, ExplorerBreakpointService, ResolveRecordTypeIcon } from '@memberjunction/ng-shared';
4
+ import { BaseResourceComponent, HomeAppPinService, NavigationService, IsRecordTabsStyle, IsRecordsTabConfiguration, IsRecordsRegionTab, IsRecordDockedToWorkspace, RECORD_DOCKED_TO_WORKSPACE_KEY, GetRecordSourceContext, SafeDetectChanges, ExplorerBreakpointService, ResolveRecordTypeIcon, ResolveRecordTabDisplayTitle, GetDeletedRecordFromEvent, RecordTabShowsRecord, RemoveDeletedRecordFromOrigin, OriginToTabConfiguration, OriginHasReturnTarget, NormalizeRecordIdForMatch } from '@memberjunction/ng-shared';
5
5
  import { ResourceData, ResourcePermissionEngine } from '@memberjunction/core-entities';
6
6
  import { RecordOriginCrumbComponent } from '../record-open/record-origin-crumb.component';
7
7
  import { RecordSwitcherService } from '../record-open/record-switcher.service';
@@ -216,6 +216,7 @@ export class TabContainerComponent extends BaseAngularComponent {
216
216
  layoutInitError = this.LayoutInitError;
217
217
  pinService = inject(HomeAppPinService);
218
218
  navigationService = inject(NavigationService);
219
+ tabService = inject(TabService);
219
220
  recordSwitcher = inject(RecordSwitcherService);
220
221
  subscriptions = [];
221
222
  layoutInitRetryCount = 0;
@@ -418,7 +419,7 @@ export class TabContainerComponent extends BaseAngularComponent {
418
419
  const activeTab = config.tabs.find(t => t.id === config.activeTabId);
419
420
  if (activeTab && this.isRecordTab(activeTab)) {
420
421
  const app = this.appManager.GetAppById(activeTab.applicationId);
421
- this.RecordBarTitle = activeTab.title;
422
+ this.RecordBarTitle = this.resolveTabTitle(activeTab);
422
423
  this.RecordBarIcon = ResolveRecordTypeIcon(activeTab.configuration, this.ProviderToUse);
423
424
  this.RecordBarColor = app?.GetColor() || DEFAULT_APP_COLOR;
424
425
  }
@@ -445,13 +446,16 @@ export class TabContainerComponent extends BaseAngularComponent {
445
446
  : null;
446
447
  return {
447
448
  TabId: tab.id,
448
- Title: tab.title,
449
+ Title: this.resolveTabTitle(tab),
449
450
  Icon: ResolveRecordTypeIcon(tab.configuration, this.ProviderToUse),
450
451
  Color: app?.GetColor() || DEFAULT_APP_COLOR,
451
452
  OriginLabel: originLabel,
452
453
  IsActive: tab.id === activeTabId
453
454
  };
454
455
  }
456
+ resolveTabTitle(tab) {
457
+ return ResolveRecordTabDisplayTitle(tab, this.ProviderToUse) || tab.title;
458
+ }
455
459
  /** True while the RECORDS region is the visible surface (active tab is a record) */
456
460
  ShowRecordsRegion = false;
457
461
  /**
@@ -473,6 +477,7 @@ export class TabContainerComponent extends BaseAngularComponent {
473
477
  flushRegionCd() {
474
478
  SafeDetectChanges(this.cdr);
475
479
  }
480
+ recordsLayoutInitRetryCount = 0;
476
481
  /**
477
482
  * Keep the RECORDS region in lockstep with the workspace configuration:
478
483
  * resolve region visibility (active tab is a record), lazily initialize the
@@ -493,6 +498,13 @@ export class TabContainerComponent extends BaseAngularComponent {
493
498
  const recordTabs = config.tabs.filter(t => this.isRecordTab(t));
494
499
  const activeTab = config.tabs.find(t => t.id === config.activeTabId);
495
500
  const showing = !!activeTab && this.isRecordTab(activeTab);
501
+ if (showing !== this.ShowRecordsRegion) {
502
+ this.ShowRecordsRegion = showing;
503
+ // Flush NOW — the visibility class must land BEFORE records GL init/tab
504
+ // creation so synchronous TabShown events see ShowRecordsRegion=true and
505
+ // load the active record immediately instead of parking it.
506
+ this.flushRegionCd();
507
+ }
496
508
  // EAGER init + sync on EVERY emission. The region container always keeps
497
509
  // its full layout box (visibility-hidden, never display:none — see the
498
510
  // component CSS), so the records GL can initialize and reconcile even
@@ -506,14 +518,8 @@ export class TabContainerComponent extends BaseAngularComponent {
506
518
  if (this.recordsLayoutInitialized) {
507
519
  this.syncRecordsTabs(recordTabs);
508
520
  }
509
- if (showing !== this.ShowRecordsRegion) {
510
- this.ShowRecordsRegion = showing;
511
- // Flush NOW — the visibility class must land in this pass, not
512
- // whenever the next unrelated emission happens to run CD.
513
- this.flushRegionCd();
514
- if (showing) {
515
- this.flushPendingRecordShows();
516
- }
521
+ if (this.ShowRecordsRegion) {
522
+ this.flushPendingRecordShows();
517
523
  }
518
524
  if (showing && activeTab && this.recordsLayoutInitialized) {
519
525
  this.focusRecordsTab(activeTab.id);
@@ -546,7 +552,7 @@ export class TabContainerComponent extends BaseAngularComponent {
546
552
  const app = this.appManager.GetAppById(tab.applicationId);
547
553
  this.recordsLayoutManager.UpdateTabStyle(tab.id, {
548
554
  isPinned: tab.isPinned,
549
- title: tab.title,
555
+ title: this.resolveTabTitle(tab),
550
556
  appColor: app?.GetColor() || DEFAULT_APP_COLOR,
551
557
  typeIcon: this.resolveTabTypeIcon(tab)
552
558
  });
@@ -736,17 +742,24 @@ export class TabContainerComponent extends BaseAngularComponent {
736
742
  return;
737
743
  }
738
744
  const container = this.RecordsGlContainer?.nativeElement;
739
- if (!container) {
740
- return; // Template not settled yet — the next sync pass retries
741
- }
742
- // NEVER initialize GL inside a zero-size (hidden) container — it bakes a
743
- // 0x0 internal size into every stack it later creates and the region
744
- // renders collapsed forever. Happens when the region flip is reverted
745
- // (activation stolen) before this deferred init runs; staying
746
- // uninitialized is safe — the next real show retries.
747
- if (container.getBoundingClientRect().height === 0) {
745
+ if (!container || container.getBoundingClientRect().height === 0) {
746
+ // NEVER initialize GL inside a missing or zero-size (hidden) container —
747
+ // it bakes a 0x0 internal size into every stack it later creates and the
748
+ // region renders collapsed forever. If the records region IS the active
749
+ // surface (e.g. startup deep link to a record before layout settles),
750
+ // schedule a bounded retry so initialization is not stranded.
751
+ if (this.ShowRecordsRegion && this.recordsLayoutInitRetryCount < this.MAX_LAYOUT_INIT_RETRIES) {
752
+ this.recordsLayoutInitRetryCount++;
753
+ setTimeout(() => {
754
+ const latest = this.workspaceManager.GetConfiguration();
755
+ if (latest && !this.recordsLayoutInitialized) {
756
+ this.syncRecordsRegion(latest);
757
+ }
758
+ }, 50);
759
+ }
748
760
  return;
749
761
  }
762
+ this.recordsLayoutInitRetryCount = 0;
750
763
  // Mobile: headerless GL — the record bar replaces the strip
751
764
  this.recordsLayoutManager.Initialize(container, { HideHeaders: this.mobileRecordsActive });
752
765
  this.recordsLayoutInitialized = true;
@@ -765,6 +778,14 @@ export class TabContainerComponent extends BaseAngularComponent {
765
778
  // loading such a layout would render a ghost pane for a closed record.
766
779
  if (layoutToLoad?.root && recordTabs.length > 0 && this.layoutCoversExactTabSet(layoutToLoad.root, recordTabs)) {
767
780
  if (this.recordsLayoutManager.LoadLayout(layoutToLoad)) {
781
+ recordTabs.forEach(tab => this.updateTabDisplayName(tab));
782
+ setTimeout(() => {
783
+ const latest = this.workspaceManager.GetConfiguration();
784
+ if (latest?.activeTabId && this.ShowRecordsRegion) {
785
+ this.focusRecordsTab(latest.activeTabId);
786
+ }
787
+ this.recordsLayoutManager.updateSize();
788
+ }, 50);
768
789
  return;
769
790
  }
770
791
  }
@@ -824,7 +845,7 @@ export class TabContainerComponent extends BaseAngularComponent {
824
845
  tabId: tab.id,
825
846
  appId: tab.applicationId,
826
847
  appColor: app?.GetColor() || DEFAULT_APP_COLOR,
827
- title: tab.title,
848
+ title: this.resolveTabTitle(tab),
828
849
  route: tab.configuration['route'] || '',
829
850
  isPinned: tab.isPinned,
830
851
  isLoaded: false,
@@ -874,6 +895,23 @@ export class TabContainerComponent extends BaseAngularComponent {
874
895
  this.ShowContextMenu(event.x, event.y, event.tabId, event.anchorEl);
875
896
  }));
876
897
  this.wireRecordsLayoutEvents();
898
+ // A record deleted in this session: close every tab showing it and move the
899
+ // user off it (see HandleRecordDeleted). Remote deletes are left to the
900
+ // record tab itself, which shows a banner rather than vanishing under the user.
901
+ this.subscriptions.push(MJGlobal.Instance.GetEventListener().subscribe(event => {
902
+ // Read the key NOW: right after raising 'delete' the entity calls
903
+ // NewRecord() and its key is gone.
904
+ const deleted = GetDeletedRecordFromEvent(event);
905
+ if (!deleted || deleted.Remote) {
906
+ return;
907
+ }
908
+ // Deferred one macrotask: the event fires inside BaseEntity.Delete(), and
909
+ // the deleting form still has to finish (success toast, RecordDeleted
910
+ // handlers) before its tab is torn down.
911
+ setTimeout(() => {
912
+ this.HandleRecordDeleted(deleted).catch(err => LogError(err));
913
+ }, 0);
914
+ }));
877
915
  // Breakpoint crossings rebuild the records GL in the other chrome mode
878
916
  // (headerless+flattened vs full strip+splits). Skip-if-equal: the
879
917
  // BehaviorSubject replays the current value on subscribe.
@@ -1036,6 +1074,11 @@ export class TabContainerComponent extends BaseAngularComponent {
1036
1074
  }));
1037
1075
  }
1038
1076
  ngAfterViewInit() {
1077
+ const config = this.workspaceManager.GetConfiguration();
1078
+ if (config) {
1079
+ this.syncRecordsRegion(config);
1080
+ this.updateRecordSurfaceState(config);
1081
+ }
1039
1082
  // Initialize Golden Layout only if we're not in single-resource mode
1040
1083
  if (!this.UseSingleResourceMode) {
1041
1084
  this.initializeGoldenLayout();
@@ -1425,7 +1468,7 @@ export class TabContainerComponent extends BaseAngularComponent {
1425
1468
  const driverClass = resourceData.Configuration?.resourceTypeDriverClass || resourceData.ResourceType;
1426
1469
  // Entity name discriminates between "new record" tabs of different entities (all
1427
1470
  // have empty ResourceRecordID otherwise). For non-Records resources this is undefined.
1428
- const cacheDiscriminator = resourceData.Configuration?.Entity;
1471
+ const cacheDiscriminator = this.resolveCacheDiscriminator(resourceData.Configuration);
1429
1472
  // **OPTIMIZATION: Check cache first to reuse existing loaded component**
1430
1473
  const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId, cacheDiscriminator);
1431
1474
  if (cached) {
@@ -1727,13 +1770,26 @@ export class TabContainerComponent extends BaseAngularComponent {
1727
1770
  const config = this.workspaceManager.GetConfiguration();
1728
1771
  const tab = config?.tabs.find(t => t.id === tabId);
1729
1772
  const isLastTab = config?.tabs.length === 1 && config.tabs[0].id === tabId;
1730
- // DESTROY (not just detach) the cached component before closing. The default
1731
- // tab-close path detaches and keeps components alive for reuse — but the
1732
- // discarded form is exactly what we DON'T want to keep: it holds a stale
1733
- // BaseEntity in view mode. The next "Create New Record" click for the same
1734
- // entity would otherwise hit the cache discriminator lookup, find this stale
1735
- // component, and reattach it — surfacing a blank form in view mode instead
1736
- // of a fresh edit-mode form. Destroy here forces a cache miss on the next click.
1773
+ this.closeTabDestroyingComponent(tabId);
1774
+ if (isLastTab && tab) {
1775
+ // CloseTab kept the tab around but unpinned it. Replace it with the app's
1776
+ // default tab so the user lands on a meaningful surface (typically the
1777
+ // home dashboard) instead of staying on the discarded form.
1778
+ await this.openAppDefaultTab(tab.applicationId);
1779
+ }
1780
+ }
1781
+ /**
1782
+ * Close a tab and DESTROY (not just detach) its cached component.
1783
+ *
1784
+ * The default tab-close path detaches and keeps components alive for reuse —
1785
+ * but a discarded or deleted record's form is exactly what we DON'T want to
1786
+ * keep: it holds a stale BaseEntity in view mode. The next "Create New Record"
1787
+ * click for the same entity would otherwise hit the cache discriminator
1788
+ * lookup, find this stale component, and reattach it — surfacing a blank form
1789
+ * in view mode instead of a fresh edit-mode form. Destroy here forces a cache
1790
+ * miss on the next open.
1791
+ */
1792
+ closeTabDestroyingComponent(tabId) {
1737
1793
  if (this.singleResourceCacheIdentity?.tabId === tabId) {
1738
1794
  // Single-resource mode: the component is tracked via singleResource* fields,
1739
1795
  // and its cache entry is currently marked attached. Use the identity directly
@@ -1744,7 +1800,7 @@ export class TabContainerComponent extends BaseAngularComponent {
1744
1800
  this.singleResourceComponentRef = null;
1745
1801
  this.singleResourceCacheIdentity = null;
1746
1802
  // Clear the host container's DOM so the user isn't briefly looking at the
1747
- // destroyed component's wrapper between CloseTab and the default-tab load.
1803
+ // destroyed component's wrapper between CloseTab and the next tab's load.
1748
1804
  const directContainer = this.DirectContentContainer?.nativeElement;
1749
1805
  if (directContainer) {
1750
1806
  while (directContainer.firstChild)
@@ -1758,18 +1814,78 @@ export class TabContainerComponent extends BaseAngularComponent {
1758
1814
  this.componentRefs.delete(tabId);
1759
1815
  }
1760
1816
  this.workspaceManager.CloseTab(tabId);
1761
- if (isLastTab && tab) {
1762
- // CloseTab kept the tab around but unpinned it. Replace it with the app's
1763
- // default tab so the user lands on a meaningful surface (typically the
1764
- // home dashboard) instead of staying on the discarded form.
1765
- const app = this.appManager.GetAppById(tab.applicationId);
1766
- if (app) {
1767
- const defaultTabRequest = await app.CreateDefaultTab();
1768
- if (defaultTabRequest) {
1769
- this.workspaceManager.OpenTab(defaultTabRequest, app.GetColor());
1770
- }
1817
+ }
1818
+ /** Open an app's default tab (its home / landing surface), if it has one. */
1819
+ async openAppDefaultTab(appId) {
1820
+ const app = this.appManager.GetAppById(appId);
1821
+ if (app) {
1822
+ const defaultTabRequest = await app.CreateDefaultTab();
1823
+ if (defaultTabRequest) {
1824
+ this.workspaceManager.OpenTab(defaultTabRequest, app.GetColor());
1825
+ }
1826
+ }
1827
+ }
1828
+ /**
1829
+ * A record was deleted in this session — from its own form, a custom form, a
1830
+ * grid row, or any other `BaseEntity.Delete()`. Nothing may keep showing it:
1831
+ *
1832
+ * 1. Other record tabs whose origin chain names it get the record spliced out,
1833
+ * so their crumb returns to where the user was before it.
1834
+ * 2. Every record tab showing it closes, its cached form destroyed.
1835
+ * 3. If one of those was the ACTIVE tab, the user is taken back to where it
1836
+ * was opened from; with no origin (e.g. loaded straight from a URL), to the
1837
+ * Home dashboard, or the tab's own app landing page when there is no Home
1838
+ * app. Its URL's history entry is replaced so browser Back can't reopen it.
1839
+ *
1840
+ * Inactive tabs showing it just close: the user wasn't looking at them.
1841
+ */
1842
+ async HandleRecordDeleted(record) {
1843
+ const config = this.workspaceManager.GetConfiguration();
1844
+ if (!config) {
1845
+ return;
1846
+ }
1847
+ const showing = config.tabs.filter(t => RecordTabShowsRecord(t, record));
1848
+ const showingIds = new Set(showing.map(t => t.id));
1849
+ for (const tab of config.tabs) {
1850
+ if (showingIds.has(tab.id) || !IsRecordsTabConfiguration(tab.configuration)) {
1851
+ continue;
1771
1852
  }
1853
+ const origin = GetRecordSourceContext(tab.configuration) ?? undefined;
1854
+ const pruned = RemoveDeletedRecordFromOrigin(origin, record);
1855
+ if (pruned !== origin) {
1856
+ this.workspaceManager.UpdateTabConfiguration(tab.id, OriginToTabConfiguration(pruned));
1857
+ }
1858
+ }
1859
+ if (showing.length === 0) {
1860
+ return;
1861
+ }
1862
+ const activeTab = showing.find(t => t.id === config.activeTabId);
1863
+ const returnTo = activeTab
1864
+ ? RemoveDeletedRecordFromOrigin(GetRecordSourceContext(activeTab.configuration) ?? undefined, record)
1865
+ : undefined;
1866
+ if (activeTab) {
1867
+ // Mark by what the tab's URL carries (its own entity string and id), not
1868
+ // the event's canonical name: the URL is built from the tab.
1869
+ const urlEntity = activeTab.configuration?.['Entity'];
1870
+ this.tabService.MarkRecordUrlForReplacement(typeof urlEntity === 'string' ? urlEntity : record.EntityName, NormalizeRecordIdForMatch(activeTab.resourceRecordId || record.RecordId));
1871
+ }
1872
+ for (const tab of showing) {
1873
+ this.closeTabDestroyingComponent(tab.id);
1874
+ }
1875
+ if (activeTab) {
1876
+ await this.leaveDeletedRecord(activeTab, returnTo);
1877
+ }
1878
+ }
1879
+ /** Where the user goes after the record they were looking at is deleted. */
1880
+ async leaveDeletedRecord(deletedTab, origin) {
1881
+ if (OriginHasReturnTarget(origin)) {
1882
+ await this.navigationService.ReturnToRecordSource(origin);
1883
+ return;
1884
+ }
1885
+ if (await this.navigationService.OpenHomeDashboard()) {
1886
+ return;
1772
1887
  }
1888
+ await this.openAppDefaultTab(deletedTab.applicationId);
1773
1889
  }
1774
1890
  /**
1775
1891
  * Generate a signature for tab content to detect when content changes
@@ -1780,13 +1896,15 @@ export class TabContainerComponent extends BaseAngularComponent {
1780
1896
  // IMPORTANT: Check both resourceRecordId AND configuration.recordId
1781
1897
  // because for nav items, the recordId is stored in configuration, not resourceRecordId
1782
1898
  const effectiveRecordId = tab.resourceRecordId || tab.configuration?.recordId || '';
1899
+ const effectiveAbv = tab.configuration?.['AlternateBaseView'] || tab.configuration?.['alternateBaseView'] || '';
1783
1900
  const parts = [
1784
1901
  tab.applicationId,
1785
1902
  tab.configuration?.resourceType || '',
1786
1903
  tab.configuration?.driverClass || '',
1787
1904
  tab.configuration?.Entity || '', // Include Entity name for Records resource type
1788
1905
  effectiveRecordId,
1789
- tab.configuration?.route || ''
1906
+ tab.configuration?.route || '',
1907
+ effectiveAbv
1790
1908
  ];
1791
1909
  return parts.join('|');
1792
1910
  }
@@ -1800,7 +1918,7 @@ export class TabContainerComponent extends BaseAngularComponent {
1800
1918
  tabId: tab.id,
1801
1919
  appId: tab.applicationId,
1802
1920
  appColor,
1803
- title: tab.title,
1921
+ title: this.resolveTabTitle(tab),
1804
1922
  route: tab.configuration['route'] || '',
1805
1923
  isPinned: tab.isPinned,
1806
1924
  isLoaded: false,
@@ -1905,7 +2023,7 @@ export class TabContainerComponent extends BaseAngularComponent {
1905
2023
  const driverClass = resourceData.Configuration?.resourceTypeDriverClass || resourceData.ResourceType;
1906
2024
  // Discriminate distinct "new record" tabs of different entities (all have empty
1907
2025
  // ResourceRecordID otherwise — would silently collide in the cache).
1908
- const cacheDiscriminator = resourceData.Configuration?.Entity;
2026
+ const cacheDiscriminator = this.resolveCacheDiscriminator(resourceData.Configuration);
1909
2027
  // Check if we have a cached component for this resource
1910
2028
  const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', tab.applicationId, cacheDiscriminator);
1911
2029
  if (cached) {
@@ -2400,6 +2518,8 @@ export class TabContainerComponent extends BaseAngularComponent {
2400
2518
  // reload from being handed back the component it just detached.
2401
2519
  const existingEntity = existingResourceData?.Configuration?.Entity;
2402
2520
  const newEntity = tab.configuration['Entity'];
2521
+ const existingAbv = (existingResourceData?.Configuration?.['AlternateBaseView'] ?? existingResourceData?.Configuration?.['alternateBaseView']) || '';
2522
+ const newAbv = (tab.configuration['AlternateBaseView'] ?? tab.configuration['alternateBaseView']) || '';
2403
2523
  // The record id comparison accepts the URL-segment form ResourceRecordSaved writes into
2404
2524
  // the component's Data ('ID|<uuid>' for a tab that keeps '<uuid>'). An exact comparison
2405
2525
  // reloads the same cached component on every configuration emission, and each reload
@@ -2408,6 +2528,7 @@ export class TabContainerComponent extends BaseAngularComponent {
2408
2528
  existingResourceData?.Configuration?.applicationId !== tab.applicationId ||
2409
2529
  !TabContainerComponent.IsSameRecordId(existingRecordId, newRecordId) ||
2410
2530
  existingEntity !== newEntity ||
2531
+ existingAbv !== newAbv ||
2411
2532
  (tab.configuration['resourceType'] === 'Custom' && existingDriverClass !== newDriverClass);
2412
2533
  if (needsReload) {
2413
2534
  // Clean up old component
@@ -2435,7 +2556,7 @@ export class TabContainerComponent extends BaseAngularComponent {
2435
2556
  const app = this.appManager.GetAppById(tab.applicationId);
2436
2557
  const styleUpdate = {
2437
2558
  isPinned: tab.isPinned,
2438
- title: tab.title,
2559
+ title: this.resolveTabTitle(tab),
2439
2560
  appColor: app?.GetColor() || DEFAULT_APP_COLOR
2440
2561
  };
2441
2562
  if (IsRecordsTabConfiguration(tab.configuration)) {
@@ -2919,6 +3040,26 @@ export class TabContainerComponent extends BaseAngularComponent {
2919
3040
  // Unblock the shell's first-load gate so the loading overlay clears.
2920
3041
  this.emitFirstLoadCompleteOnce();
2921
3042
  }
3043
+ resolveCacheDiscriminator(configuration) {
3044
+ if (typeof ComponentCacheManager.GetCacheDiscriminator === 'function') {
3045
+ return ComponentCacheManager.GetCacheDiscriminator(configuration);
3046
+ }
3047
+ if (!configuration) {
3048
+ return undefined;
3049
+ }
3050
+ const entity = typeof configuration['Entity'] === 'string' && configuration['Entity'].trim().length > 0
3051
+ ? configuration['Entity'].trim()
3052
+ : '';
3053
+ const rawAbv = configuration['AlternateBaseView'];
3054
+ const abv = typeof rawAbv === 'string' && rawAbv.trim().length > 0 ? rawAbv.trim() : '';
3055
+ if (!entity && !abv) {
3056
+ return undefined;
3057
+ }
3058
+ if (entity && abv) {
3059
+ return `${entity}|abv:${abv}`;
3060
+ }
3061
+ return entity || `abv:${abv}`;
3062
+ }
2922
3063
  static ɵfac = function TabContainerComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || TabContainerComponent)(i0.ɵɵdirectiveInject(i1.GoldenLayoutManager), i0.ɵɵdirectiveInject(i1.WorkspaceStateManager), i0.ɵɵdirectiveInject(i1.ApplicationManager), i0.ɵɵdirectiveInject(i0.ApplicationRef), i0.ɵɵdirectiveInject(i0.EnvironmentInjector), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
2923
3064
  static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: TabContainerComponent, selectors: [["mj-tab-container"]], viewQuery: function TabContainerComponent_Query(rf, ctx) { if (rf & 1) {
2924
3065
  i0.ɵɵviewQuery(_c0, 5)(_c1, 5)(_c2, 5);
@@ -2971,5 +3112,5 @@ export class TabContainerComponent extends BaseAngularComponent {
2971
3112
  type: HostListener,
2972
3113
  args: ['window:resize']
2973
3114
  }] }); })();
2974
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(TabContainerComponent, { className: "TabContainerComponent", filePath: "src/lib/shell/components/tabs/tab-container.component.ts", lineNumber: 64 }); })();
3115
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(TabContainerComponent, { className: "TabContainerComponent", filePath: "src/lib/shell/components/tabs/tab-container.component.ts", lineNumber: 68 }); })();
2975
3116
  //# sourceMappingURL=tab-container.component.js.map