@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.
- package/dist/generated/lazy-feature-config.d.ts +1 -1
- package/dist/generated/lazy-feature-config.d.ts.map +1 -1
- package/dist/generated/lazy-feature-config.js +4 -2
- package/dist/generated/lazy-feature-config.js.map +1 -1
- package/dist/lib/__tests__/form-resolver.service.test.d.ts +2 -0
- package/dist/lib/__tests__/form-resolver.service.test.d.ts.map +1 -0
- package/dist/lib/__tests__/form-resolver.service.test.js +258 -0
- package/dist/lib/__tests__/form-resolver.service.test.js.map +1 -0
- package/dist/lib/resource-wrappers/artifact-resource.component.d.ts +10 -0
- package/dist/lib/resource-wrappers/artifact-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/artifact-resource.component.js +15 -5
- package/dist/lib/resource-wrappers/artifact-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/record-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/record-resource.component.js +22 -6
- package/dist/lib/resource-wrappers/record-resource.component.js.map +1 -1
- package/dist/lib/services/form-resolver.service.d.ts +139 -0
- package/dist/lib/services/form-resolver.service.d.ts.map +1 -0
- package/dist/lib/services/form-resolver.service.js +235 -0
- package/dist/lib/services/form-resolver.service.js.map +1 -0
- package/dist/lib/shell/components/header/app-nav.component.d.ts.map +1 -1
- package/dist/lib/shell/components/header/app-nav.component.js +12 -0
- package/dist/lib/shell/components/header/app-nav.component.js.map +1 -1
- package/dist/lib/shell/components/tabs/component-cache-manager.d.ts +24 -5
- package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
- package/dist/lib/shell/components/tabs/component-cache-manager.js +58 -14
- package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts +42 -0
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.js +186 -12
- package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
- package/dist/lib/single-record/single-record.component.d.ts +41 -3
- package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
- package/dist/lib/single-record/single-record.component.js +192 -23
- package/dist/lib/single-record/single-record.component.js.map +1 -1
- 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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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) => {
|