UNPKG

@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
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