@memberjunction/ng-explorer-core 5.37.0 → 5.38.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 (35) hide show
  1. package/dist/generated/lazy-feature-config.d.ts +1 -1
  2. package/dist/generated/lazy-feature-config.d.ts.map +1 -1
  3. package/dist/generated/lazy-feature-config.js +4 -2
  4. package/dist/generated/lazy-feature-config.js.map +1 -1
  5. package/dist/lib/__tests__/form-resolver.service.test.d.ts +2 -0
  6. package/dist/lib/__tests__/form-resolver.service.test.d.ts.map +1 -0
  7. package/dist/lib/__tests__/form-resolver.service.test.js +258 -0
  8. package/dist/lib/__tests__/form-resolver.service.test.js.map +1 -0
  9. package/dist/lib/resource-wrappers/artifact-resource.component.d.ts +10 -0
  10. package/dist/lib/resource-wrappers/artifact-resource.component.d.ts.map +1 -1
  11. package/dist/lib/resource-wrappers/artifact-resource.component.js +15 -5
  12. package/dist/lib/resource-wrappers/artifact-resource.component.js.map +1 -1
  13. package/dist/lib/resource-wrappers/record-resource.component.d.ts.map +1 -1
  14. package/dist/lib/resource-wrappers/record-resource.component.js +22 -6
  15. package/dist/lib/resource-wrappers/record-resource.component.js.map +1 -1
  16. package/dist/lib/services/form-resolver.service.d.ts +139 -0
  17. package/dist/lib/services/form-resolver.service.d.ts.map +1 -0
  18. package/dist/lib/services/form-resolver.service.js +235 -0
  19. package/dist/lib/services/form-resolver.service.js.map +1 -0
  20. package/dist/lib/shell/components/header/app-nav.component.d.ts.map +1 -1
  21. package/dist/lib/shell/components/header/app-nav.component.js +12 -0
  22. package/dist/lib/shell/components/header/app-nav.component.js.map +1 -1
  23. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts +24 -5
  24. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
  25. package/dist/lib/shell/components/tabs/component-cache-manager.js +58 -14
  26. package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
  27. package/dist/lib/shell/components/tabs/tab-container.component.d.ts +42 -0
  28. package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
  29. package/dist/lib/shell/components/tabs/tab-container.component.js +186 -12
  30. package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
  31. package/dist/lib/single-record/single-record.component.d.ts +41 -3
  32. package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
  33. package/dist/lib/single-record/single-record.component.js +192 -23
  34. package/dist/lib/single-record/single-record.component.js.map +1 -1
  35. package/package.json +46 -45
@@ -504,21 +504,24 @@ export class TabContainerComponent extends BaseAngularComponent {
504
504
  }
505
505
  // Get driver class for component lookup
506
506
  const driverClass = resourceData.Configuration?.resourceTypeDriverClass || resourceData.ResourceType;
507
+ // Entity name discriminates between "new record" tabs of different entities (all
508
+ // have empty ResourceRecordID otherwise). For non-Records resources this is undefined.
509
+ const cacheDiscriminator = resourceData.Configuration?.Entity;
507
510
  // **OPTIMIZATION: Check cache first to reuse existing loaded component**
508
- const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId);
511
+ const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId, cacheDiscriminator);
509
512
  if (cached) {
510
513
  // Clean up previous single-resource component (if different)
511
514
  this.cleanupSingleResourceComponent();
512
515
  // Mark cached component as attached to this tab (it was detached / available for reuse).
513
516
  // IMPORTANT: We use markAsAttached here, NOT markAsDetached — the component is being
514
517
  // reattached to the DOM and should NOT be eligible for LRU eviction.
515
- this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId, activeTab.id);
518
+ this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', activeTab.applicationId, activeTab.id, cacheDiscriminator);
516
519
  // Reattach the cached wrapper element to single-resource container
517
520
  cached.wrapperElement.style.height = "100%"; // Ensure full height
518
521
  container.appendChild(cached.wrapperElement);
519
522
  // Store reference and identity for cleanup/detachment
520
523
  this.singleResourceComponentRef = cached.componentRef;
521
- this.singleResourceCacheIdentity = { driverClass, recordId: resourceData.ResourceRecordID || '', appId: activeTab.applicationId, tabId: activeTab.id };
524
+ this.singleResourceCacheIdentity = { driverClass, recordId: resourceData.ResourceRecordID || '', appId: activeTab.applicationId, tabId: activeTab.id, discriminator: cacheDiscriminator };
522
525
  // Reconcile the cached component's preserved queryParams with any INCOMING navigation
523
526
  // intent already on the tab config (e.g. a Home pin / deep link that targeted a specific
524
527
  // conversation via SwitchToApp before we got here).
@@ -592,6 +595,17 @@ export class TabContainerComponent extends BaseAngularComponent {
592
595
  }
593
596
  }
594
597
  };
598
+ // When a record is saved, re-key the tab and cache (new-record → saved transition)
599
+ // and refresh the tab title to reflect the entity's current display name.
600
+ instance.ResourceRecordSavedEvent = (entity) => {
601
+ this.handleResourceRecordSaved(driverClass, activeTab.applicationId, activeTab.id, instance, entity);
602
+ };
603
+ // Resource asked to be closed (e.g., user discarded a brand-new record — there's
604
+ // no actual record to view, so leaving the tab open serves no purpose and would
605
+ // also poison the cache for the next "Create New" click of the same entity).
606
+ instance.ResourceCloseRequestedEvent = () => {
607
+ this.handleResourceCloseRequested(activeTab.id, instance);
608
+ };
595
609
  // Get the native element and append to container
596
610
  const nativeElement = componentRef.hostView.rootNodes[0];
597
611
  container.appendChild(nativeElement);
@@ -605,7 +619,7 @@ export class TabContainerComponent extends BaseAngularComponent {
605
619
  this.cacheManager.cacheComponent(componentRef, wrapperElement, resourceData, activeTab.id);
606
620
  // Store reference and identity for cleanup/detachment
607
621
  this.singleResourceComponentRef = componentRef;
608
- this.singleResourceCacheIdentity = { driverClass, recordId: resourceData.ResourceRecordID || '', appId: activeTab.applicationId, tabId: activeTab.id };
622
+ this.singleResourceCacheIdentity = { driverClass, recordId: resourceData.ResourceRecordID || '', appId: activeTab.applicationId, tabId: activeTab.id, discriminator: cacheDiscriminator };
609
623
  }
610
624
  /**
611
625
  * Clean up single-resource mode component
@@ -654,9 +668,9 @@ export class TabContainerComponent extends BaseAngularComponent {
654
668
  cleanupSingleResourceComponent() {
655
669
  if (this.singleResourceComponentRef) {
656
670
  if (this.singleResourceCacheIdentity) {
657
- const { driverClass, recordId, appId } = this.singleResourceCacheIdentity;
671
+ const { driverClass, recordId, appId, discriminator } = this.singleResourceCacheIdentity;
658
672
  // Mark as DETACHED by resource identity — the ONE consistent key used everywhere.
659
- this.cacheManager.markAsDetached(driverClass, recordId, appId);
673
+ this.cacheManager.markAsDetached(driverClass, recordId, appId, discriminator);
660
674
  }
661
675
  this.singleResourceComponentRef = null;
662
676
  this.singleResourceCacheIdentity = null;
@@ -671,6 +685,162 @@ export class TabContainerComponent extends BaseAngularComponent {
671
685
  }
672
686
  }
673
687
  }
688
+ /**
689
+ * Handle a record-saved event from a hosted resource component.
690
+ *
691
+ * Does two things on every save:
692
+ *
693
+ * 1. **Re-key new-record tabs.** When a tab opened as "Create New Record" (empty
694
+ * `resourceRecordId`) transitions to a saved record, we update both the workspace
695
+ * tab's id and the component-cache key to the new PK. Without this, the next
696
+ * `OpenNewEntityRecord(<sameEntity>)` would match this tab (both have empty
697
+ * `resourceRecordId`) and focus the stale form instead of opening a fresh one.
698
+ * For existing records being re-saved, this step is a no-op.
699
+ *
700
+ * 2. **Refresh the tab title.** Whether new or existing, the entity's display name
701
+ * (typically its `Name` field) may have changed. We call the resource component's
702
+ * `GetResourceDisplayName` and push the result to the tab — so a tab that read
703
+ * "New Foo Record" before save now reads the actual entered name.
704
+ */
705
+ handleResourceRecordSaved(driverClass, appId, tabId, instance, entity) {
706
+ const tab = this.workspaceManager.GetTab(tabId);
707
+ if (!tab) {
708
+ return;
709
+ }
710
+ const oldRecordId = tab.resourceRecordId || '';
711
+ // ToURLSegment, NOT ToString — must match the format NavigationService.OpenEntityRecord
712
+ // uses and that EntityRecordResource.GetPrimaryKey expects via LoadFromURLSegment.
713
+ const newRecordId = entity?.PrimaryKey?.ToURLSegment?.() ?? '';
714
+ const isNewRecordTransition = oldRecordId === '' && !!newRecordId;
715
+ // The cache entry for an empty-recordId tab was keyed with the entity name as
716
+ // discriminator (to avoid new-record collisions across entities). Re-key has to
717
+ // pass that same old discriminator to find the entry. After save the record has
718
+ // a real PK, so the new key needs no discriminator.
719
+ const oldDiscriminator = tab.configuration?.['Entity'];
720
+ if (isNewRecordTransition) {
721
+ // Re-key the cache entry first, so when the workspace config emission triggers
722
+ // signature/needsReload checks below, the cache lookup at the new id succeeds.
723
+ this.cacheManager.rekeyComponent(driverClass, oldRecordId, newRecordId, appId, oldDiscriminator, undefined);
724
+ // Keep the in-memory single-resource identity in sync so detach uses the correct key.
725
+ if (this.singleResourceCacheIdentity?.tabId === tabId) {
726
+ this.singleResourceCacheIdentity = {
727
+ ...this.singleResourceCacheIdentity,
728
+ recordId: newRecordId,
729
+ discriminator: undefined
730
+ };
731
+ }
732
+ // Pre-compute and stamp the new signature so the configuration-subscription path
733
+ // in single-resource mode doesn't see a "content changed" delta and trigger a
734
+ // needless reload cycle (which would destroy the currently attached component).
735
+ if (this.useSingleResourceMode && this.currentSingleResourceSignature !== null) {
736
+ const updatedTab = {
737
+ ...tab,
738
+ resourceRecordId: newRecordId,
739
+ configuration: { ...tab.configuration, recordId: newRecordId, isNew: undefined }
740
+ };
741
+ this.currentSingleResourceSignature = this.getTabContentSignature(updatedTab);
742
+ }
743
+ // Propagate the new id to the workspace tab.
744
+ this.workspaceManager.UpdateTabResourceRecordId(tabId, newRecordId);
745
+ }
746
+ // Always refresh the tab title — the entity's display-name field (typically Name)
747
+ // may have just been set or changed. Done after the rekey so the resource component's
748
+ // Data.ResourceRecordID reflects the saved PK before GetResourceDisplayName reads it.
749
+ //
750
+ // ProviderBase caches GetEntityRecordName results, so a plain call here would return
751
+ // the pre-edit name. Pre-warm the cache with a forceRefresh so the downstream read
752
+ // inside GetResourceDisplayName picks up the saved name. Only meaningful for entity
753
+ // records with a PK; other resource types' GetResourceDisplayName doesn't hit the
754
+ // record-name cache and will work either way.
755
+ void this.refreshTabTitleAfterSave(tabId, instance, entity);
756
+ }
757
+ /**
758
+ * After a save, invalidate the entity-record-name cache for the saved record and then
759
+ * push the resource component's current display name into the tab title.
760
+ *
761
+ * Errors are swallowed (logged via the inner call paths) — failing to refresh a tab
762
+ * title should never break the save flow.
763
+ */
764
+ async refreshTabTitleAfterSave(tabId, instance, entity) {
765
+ try {
766
+ const entityName = entity?.EntityInfo?.Name;
767
+ const pk = entity?.PrimaryKey;
768
+ if (entityName && pk?.HasValue) {
769
+ // forceRefresh: true overwrites the cached (pre-edit) name with the fresh one.
770
+ // GetResourceDisplayName (called next) reads the same cache and now sees fresh data.
771
+ const md = instance.ProviderToUse;
772
+ await md.GetEntityRecordName(entityName, pk, md.CurrentUser, true);
773
+ }
774
+ }
775
+ catch {
776
+ // Cache pre-warm failures are non-fatal — the title update below will fall back
777
+ // to whatever the cache has and the user can still interact with the form.
778
+ }
779
+ await this.updateTabTitleFromResource(tabId, instance, instance.Data);
780
+ }
781
+ /**
782
+ * A resource component asked to be dismissed — typically the user clicked Discard
783
+ * on a brand-new record. Behavior depends on workspace state:
784
+ *
785
+ * - **Multi-tab mode (or > 1 tab)**: just close the tab. `WorkspaceStateManager.CloseTab`
786
+ * activates the next tab, `syncTabsWithConfiguration` removes the tab from Golden
787
+ * Layout, and the user lands on whatever tab was previously active.
788
+ *
789
+ * - **Last tab in the workspace**: `CloseTab` intentionally keeps the last tab around
790
+ * (just unpins it) so the workspace is never empty — correct for the
791
+ * user-clicked-X-button case, wrong here. We want the user OFF this discarded form.
792
+ * Workaround: `CloseTab` makes the tab unpinned (= a temp tab), then we open the
793
+ * app's default tab via `CreateDefaultTab()` which goes through `OpenTab` and
794
+ * replaces the now-temp tab. End result: user lands on the app's home/default view.
795
+ */
796
+ async handleResourceCloseRequested(tabId, _instance) {
797
+ const config = this.workspaceManager.GetConfiguration();
798
+ const tab = config?.tabs.find(t => t.id === tabId);
799
+ const isLastTab = config?.tabs.length === 1 && config.tabs[0].id === tabId;
800
+ // DESTROY (not just detach) the cached component before closing. The default
801
+ // tab-close path detaches and keeps components alive for reuse — but the
802
+ // discarded form is exactly what we DON'T want to keep: it holds a stale
803
+ // BaseEntity in view mode. The next "Create New Record" click for the same
804
+ // entity would otherwise hit the cache discriminator lookup, find this stale
805
+ // component, and reattach it — surfacing a blank form in view mode instead
806
+ // of a fresh edit-mode form. Destroy here forces a cache miss on the next click.
807
+ if (this.singleResourceCacheIdentity?.tabId === tabId) {
808
+ // Single-resource mode: the component is tracked via singleResource* fields,
809
+ // and its cache entry is currently marked attached. Use the identity directly
810
+ // to destroy it (markAsDetached would clear attachedToTabId and break a
811
+ // subsequent destroyComponentByTabId lookup).
812
+ const { driverClass, recordId, appId, discriminator } = this.singleResourceCacheIdentity;
813
+ this.cacheManager.destroyComponent(driverClass, recordId, appId, discriminator);
814
+ this.singleResourceComponentRef = null;
815
+ this.singleResourceCacheIdentity = null;
816
+ // Clear the host container's DOM so the user isn't briefly looking at the
817
+ // destroyed component's wrapper between CloseTab and the default-tab load.
818
+ const directContainer = this.directContentContainer?.nativeElement;
819
+ if (directContainer) {
820
+ while (directContainer.firstChild)
821
+ directContainer.removeChild(directContainer.firstChild);
822
+ }
823
+ }
824
+ else {
825
+ // Multi-tab mode: cache entry is keyed by attachedToTabId; the convenience
826
+ // method handles the lookup.
827
+ this.cacheManager.destroyComponentByTabId(tabId);
828
+ this.componentRefs.delete(tabId);
829
+ }
830
+ this.workspaceManager.CloseTab(tabId);
831
+ if (isLastTab && tab) {
832
+ // CloseTab kept the tab around but unpinned it. Replace it with the app's
833
+ // default tab so the user lands on a meaningful surface (typically the
834
+ // home dashboard) instead of staying on the discarded form.
835
+ const app = this.appManager.GetAppById(tab.applicationId);
836
+ if (app) {
837
+ const defaultTabRequest = await app.CreateDefaultTab();
838
+ if (defaultTabRequest) {
839
+ this.workspaceManager.OpenTab(defaultTabRequest, app.GetColor());
840
+ }
841
+ }
842
+ }
843
+ }
674
844
  /**
675
845
  * Generate a signature for tab content to detect when content changes
676
846
  * This is needed because in single-resource mode, the same tab ID can have different content
@@ -753,13 +923,16 @@ export class TabContainerComponent extends BaseAngularComponent {
753
923
  glContainer.element.innerHTML = '';
754
924
  // Get driver class for cache lookup (resolves to actual component class name)
755
925
  const driverClass = resourceData.Configuration?.resourceTypeDriverClass || resourceData.ResourceType;
926
+ // Discriminate distinct "new record" tabs of different entities (all have empty
927
+ // ResourceRecordID otherwise — would silently collide in the cache).
928
+ const cacheDiscriminator = resourceData.Configuration?.Entity;
756
929
  // Check if we have a cached component for this resource
757
- const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', tab.applicationId);
930
+ const cached = this.cacheManager.getCachedComponent(driverClass, resourceData.ResourceRecordID || '', tab.applicationId, cacheDiscriminator);
758
931
  if (cached) {
759
932
  // Reattach the cached wrapper element
760
933
  glContainer.element.appendChild(cached.wrapperElement);
761
934
  // Mark as attached to this tab
762
- this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', tab.applicationId, tabId);
935
+ this.cacheManager.markAsAttached(driverClass, resourceData.ResourceRecordID || '', tab.applicationId, tabId, cacheDiscriminator);
763
936
  // Keep legacy componentRefs map updated
764
937
  this.componentRefs.set(tabId, cached.componentRef);
765
938
  // If resource is already loaded, update tab title immediately and signal
@@ -802,10 +975,11 @@ export class TabContainerComponent extends BaseAngularComponent {
802
975
  this.emitFirstLoadCompleteOnce();
803
976
  };
804
977
  instance.ResourceRecordSavedEvent = (entity) => {
805
- // Update tab title if needed
806
- if (entity && entity.Get && entity.Get('Name')) {
807
- // TODO: Implement UpdateTabTitle in WorkspaceStateManager
808
- }
978
+ this.handleResourceRecordSaved(driverClass, tab.applicationId, tabId, instance, entity);
979
+ };
980
+ // Resource asked to be closed (e.g., user discarded a brand-new record).
981
+ instance.ResourceCloseRequestedEvent = () => {
982
+ this.handleResourceCloseRequested(tabId, instance);
809
983
  };
810
984
  // Wire up display name change notifications
811
985
  instance.DisplayNameChangedEvent = (newName) => {