@memberjunction/ng-shared
Version:
MemberJunction: MJ Explorer Angular Shared Package - utility functions and other reusable elements used across other MJ Angular packages within the MJ Explorer App - do not use outside of MJ Explorer.
234 lines • 9.88 kB
JavaScript
import { Directive, Input, inject } from "@angular/core";
import { Subject } from "rxjs";
import { filter, takeUntil } from "rxjs/operators";
import { BaseNavigationComponent } from "./base-navigation-component";
import { ResourceData } from "@memberjunction/core-entities";
import { NavigationService } from "./navigation.service";
import * as i0 from "@angular/core";
export class BaseResourceComponent extends BaseNavigationComponent {
_data = new ResourceData();
_suppressQueryParamSync = false;
/** Stable key of the most recently delivered query params, used to de-duplicate
* delivery across the reactive (initial/deep-link) and popstate paths. */
_lastDeliveredParamsKey = null;
destroy$ = new Subject();
navigationService = inject(NavigationService);
/**
* Tab ID for query param notification scoping. Set by resource wrappers
* that render child dashboards, so the child knows which tab it belongs to.
* If not set, falls back to Data.Configuration.tabId.
*/
ParentTabId = null;
get Data() {
return this._data;
}
set Data(value) {
this._data = value;
}
_loadComplete = false;
get LoadComplete() {
return this._loadComplete;
}
_loadStarted = false;
get LoadStarted() {
return this._loadStarted;
}
_loadCompleteEvent = null;
get LoadCompleteEvent() {
return this._loadCompleteEvent;
}
set LoadCompleteEvent(value) {
this._loadCompleteEvent = value;
}
_loadStartedEvent = null;
get LoadStartedEvent() {
return this._loadStartedEvent;
}
set LoadStartedEvent(value) {
this._loadStartedEvent = value;
}
_resourceRecordSavedEvent = null;
get ResourceRecordSavedEvent() {
return this._resourceRecordSavedEvent;
}
set ResourceRecordSavedEvent(value) {
this._resourceRecordSavedEvent = value;
}
_resourceCloseRequestedEvent = null;
get ResourceCloseRequestedEvent() {
return this._resourceCloseRequestedEvent;
}
set ResourceCloseRequestedEvent(value) {
this._resourceCloseRequestedEvent = value;
}
_displayNameChangedEvent = null;
get DisplayNameChangedEvent() {
return this._displayNameChangedEvent;
}
set DisplayNameChangedEvent(value) {
this._displayNameChangedEvent = value;
}
ngOnInit() {
// Order matters: set up the popstate subscription first (a plain Subject — emits
// nothing on subscribe), then the reactive initial-delivery subscription (backed by
// a BehaviorSubject — emits the current params synchronously). This lets the initial
// emission claim the 'deeplink' source before any later popstate event.
this.setupQueryParamSubscription();
this.setupInitialParamDelivery();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
/**
* Called by the framework when query params change from an external source
* (browser back/forward, deep link navigation).
* Override in subclasses to react to query param changes.
* @param params The new query params from the URL
* @param source 'popstate' for back/forward, 'deeplink' for external URL entry
*/
OnQueryParamsChanged(params, source) {
// Default no-op — override in subclasses
}
/**
* Push query param changes to the URL. Creates a browser history entry.
* Safe to call during OnQueryParamsChanged — auto-suppressed to prevent loops.
*/
UpdateQueryParams(params) {
if (this._suppressQueryParamSync)
return;
this.navigationService.UpdateActiveTabQueryParams(params);
}
/**
* Read current query params from tab configuration.
* Use in initDashboard() / ngOnInit() to get initial URL state.
*/
GetQueryParams() {
return this.Data?.Configuration?.['queryParams'] || {};
}
/**
* Internal: subscribe to NavigationService query param notifications.
* Filters to only this component's tab to prevent cross-tab leakage.
* This is the explicit back/forward (popstate) path.
*/
setupQueryParamSubscription() {
this.navigationService.QueryParamChanged$
.pipe(filter(event => {
const myTabId = this.getTabId();
return !myTabId || event.TabId === myTabId;
}), takeUntil(this.destroy$))
.subscribe(event => this.deliverQueryParams(event.Params));
}
/**
* Internal: reactively deliver this tab's query params from the workspace
* BehaviorSubject. Because the source replays the current value on subscribe and
* emits future changes, this delivers initial deep-link params even when the
* component mounts (e.g. from workspace restoration) before the URL params have
* been merged into the tab configuration — closing the cold-load race that the
* popstate-only path could not.
*/
setupInitialParamDelivery() {
const tabId = this.getTabId();
if (!tabId) {
return; // No tab scope (e.g. embedded usage) — nothing to observe.
}
this.navigationService.ObserveTabQueryParams(tabId)
.pipe(takeUntil(this.destroy$))
.subscribe(params => this.deliverQueryParams(params));
}
/**
* Internal: funnel point for both the reactive and popstate paths. De-duplicates
* identical deliveries (the two paths overlap on back/forward) and labels the first
* meaningful delivery as a 'deeplink', subsequent ones as 'popstate'.
*/
deliverQueryParams(params) {
const key = this.queryParamsKey(params);
if (key === this._lastDeliveredParamsKey) {
return; // Already delivered these exact params.
}
const isInitial = this._lastDeliveredParamsKey === null;
// Don't fire an initial no-op: a component entered without deep-link params has
// nothing to apply. Leave _lastDeliveredParamsKey null so the first real params
// (whenever they arrive) are still treated as the deep-link entry.
if (isInitial && Object.keys(params).length === 0) {
return;
}
this._lastDeliveredParamsKey = key;
const source = isInitial ? 'deeplink' : 'popstate';
// try/finally ensures the suppression flag is always cleared, even if
// OnQueryParamsChanged throws.
this._suppressQueryParamSync = true;
try {
this.OnQueryParamsChanged(params, source);
}
finally {
this._suppressQueryParamSync = false;
}
}
/** Stable, order-independent string key for a query param record. */
queryParamsKey(params) {
return Object.keys(params)
.sort()
.map(k => `${k}=${params[k]}`)
.join('&');
}
/**
* Get this component's tab ID. Checks ParentTabId input first (set by resource
* wrappers for child dashboards), then falls back to Data.Configuration.tabId.
*/
getTabId() {
return this.ParentTabId || this.Data?.Configuration?.['tabId'] || '';
}
NotifyLoadComplete() {
this._loadComplete = true;
if (this._loadCompleteEvent) {
this._loadCompleteEvent();
}
}
NotifyLoadStarted() {
this._loadStarted = true;
if (this._loadStartedEvent) {
this._loadStartedEvent();
}
}
/**
* Call this to notify the tab system that the resource's display name has changed.
* The tab container will update the tab title and browser title accordingly.
*/
NotifyDisplayNameChanged(newName) {
if (this._displayNameChangedEvent) {
this._displayNameChangedEvent(newName);
}
}
ResourceRecordSaved(resourceRecordEntity) {
// Use ToURLSegment, NOT ToString. ResourceRecordID is consumed by
// EntityRecordResource.GetPrimaryKey → CompositeKey.LoadFromURLSegment, which
// expects URL-segment format (e.g. "ID|<uuid>"). ToString produces "ID=<uuid>",
// which LoadFromURLSegment treats as a raw value and double-prefixes —
// breaking any code path that re-reads this field after save.
this.Data.ResourceRecordID = resourceRecordEntity.PrimaryKey.ToURLSegment();
if (this._resourceRecordSavedEvent) {
this._resourceRecordSavedEvent(resourceRecordEntity);
}
}
/**
* Ask the host shell to close/dismiss this resource (typically: close the tab).
* Called by subclasses that hosted a form that emitted a 'dismiss' navigation
* event — most often, a brand-new record where the user clicked Discard. The
* record was never saved, the form is empty, and leaving it open serves no
* purpose. The tab-container listens for this and closes the workspace tab.
*/
NotifyCloseRequested() {
if (this._resourceCloseRequestedEvent) {
this._resourceCloseRequestedEvent();
}
}
static ɵfac = /*@__PURE__*/ (() => { let ɵBaseResourceComponent_BaseFactory; return function BaseResourceComponent_Factory(__ngFactoryType__) { return (ɵBaseResourceComponent_BaseFactory || (ɵBaseResourceComponent_BaseFactory = i0.ɵɵgetInheritedFactory(BaseResourceComponent)))(__ngFactoryType__ || BaseResourceComponent); }; })();
static ɵdir = /*@__PURE__*/ i0.ɵɵdefineDirective({ type: BaseResourceComponent, inputs: { ParentTabId: "ParentTabId" }, features: [i0.ɵɵInheritDefinitionFeature] });
}
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(BaseResourceComponent, [{
type: Directive
}], null, { ParentTabId: [{
type: Input
}] }); })();
//# sourceMappingURL=base-resource-component.js.map