@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.
325 lines • 14 kB
JavaScript
import { Injectable } from '@angular/core';
import { LogError, Metadata, StartupManager } from '@memberjunction/core';
import { ArtifactMetadataEngine, DashboardEngine, ResourcePermissionEngine } from '@memberjunction/core-entities';
import { AIEngineBase } from '@memberjunction/ai-engine-base';
import { EntityCommunicationsEngineBase } from "@memberjunction/entity-communications-base";
import { MJEventType, MJGlobal, ConvertMarkdownStringToHtmlList, InvokeManualResize } from '@memberjunction/global';
import { Subject, BehaviorSubject, firstValueFrom } from 'rxjs';
import { first, tap } from 'rxjs/operators';
import { MJNotificationService } from '@memberjunction/ng-notifications';
import { NavigationService } from './navigation.service';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-notification";
import * as i2 from "@memberjunction/ng-notifications";
export class SharedService {
notificationService;
mjNotificationsService;
injector;
static _instance;
static _loaded = false;
static _resourceTypes = [];
static isLoading$ = new BehaviorSubject(false);
tabChange = new Subject();
tabChange$ = this.tabChange.asObservable();
_navigationService = null;
constructor(notificationService, mjNotificationsService, injector) {
this.notificationService = notificationService;
this.mjNotificationsService = mjNotificationsService;
this.injector = injector;
if (SharedService._instance) {
// return existing instance which will short circuit the creation of a new instance
return SharedService._instance;
}
// first time this has been called, so return ourselves since we're in the constructor
SharedService._instance = this;
MJGlobal.Instance.GetEventListener(true).subscribe(async (event) => {
switch (event.event) {
case MJEventType.LoggedIn:
if (SharedService._loaded === false) {
// Handle app startup
await StartupManager.Instance.Startup();
await SharedService.RefreshData(false);
// Pre-warm other engines in the background (fire and forget)
// These are not needed immediately but will be ready when user navigates to
// Conversations, Dashboards, or Artifacts. The BaseEngine pattern ensures
// subsequent callers will wait for the existing load rather than starting a new one.
SharedService.preWarmEngines();
}
break;
}
});
}
static get Instance() {
return SharedService._instance;
}
/**
* Pre-warms commonly used engines in the background after login.
* This reduces perceived latency when users navigate to features like
* Conversations, Dashboards, or Artifacts. Fire-and-forget pattern -
* errors are logged but don't block the UI.
*/
static preWarmEngines() {
// AIEngineBase is the slowest - loads agents, models, prompts, etc.
// Critical for Conversations feature
AIEngineBase.Instance.Config(false).catch(err => LogError(`Failed to pre-warm AIEngineBase: ${err}`));
// ArtifactMetadataEngine is lightweight (just artifact types)
// Used by Conversations and Artifact viewer
ArtifactMetadataEngine.Instance.Config(false).catch(err => LogError(`Failed to pre-warm ArtifactMetadataEngine: ${err}`));
// DashboardEngine loads dashboard metadata
// Used when viewing dashboards
DashboardEngine.Instance.Config(false).catch(err => LogError(`Failed to pre-warm DashboardEngine: ${err}`));
EntityCommunicationsEngineBase.Instance.Config(false).catch(err => LogError(`Failed to pre-warm DashboardEngine: ${err}`));
}
/**
* Get the NavigationService singleton instance
* Lazy-loaded to avoid circular dependency issues
*/
get navigationService() {
if (!this._navigationService) {
this._navigationService = this.injector.get(NavigationService);
}
return this._navigationService;
}
/**
* Get the neutral color used for system-wide resources
*/
get ExplorerAppColor() {
return this.navigationService.ExplorerAppColor;
}
/**
* Returns the current session ID, which is automatically created when the service is instantiated.
*/
get SessionId() {
return Metadata.Provider.sessionId;
}
get ResourceTypes() {
return SharedService._resourceTypes;
}
get ViewResourceType() {
return SharedService._resourceTypes.find(rt => rt.Name.trim().toLowerCase() === 'user views');
}
get RecordResourceType() {
return SharedService._resourceTypes.find(rt => rt.Name.trim().toLowerCase() === 'records');
}
get DashboardResourceType() {
return SharedService._resourceTypes.find(rt => rt.Name.trim().toLowerCase() === 'dashboards');
}
get ReportResourceType() {
return SharedService._resourceTypes.find(rt => rt.Name.trim().toLowerCase() === 'reports');
}
get SearchResultsResourceType() {
return SharedService._resourceTypes.find(rt => rt.Name.trim().toLowerCase() === 'search results');
}
get ListResourceType() {
return SharedService._resourceTypes.find(rt => rt.Name.trim().toLowerCase() === 'lists');
}
ResourceTypeByID(id) {
return SharedService._resourceTypes.find(rt => rt.ID === id);
}
ResourceTypeByName(name) {
return SharedService._resourceTypes.find(rt => rt.Name.trim().toLowerCase() === name.trim().toLowerCase());
}
/**
* Refreshes the data for the service. If OnlyIfNeeded is true, then the data is only refreshed if it hasn't been loaded yet.
*/
static async RefreshData(OnlyIfNeeded = false) {
if (OnlyIfNeeded && SharedService._loaded) {
return;
}
const canProceed$ = SharedService.isLoading$.pipe(first(isLoading => !isLoading), tap(() => SharedService.isLoading$.next(true)));
await firstValueFrom(canProceed$);
try {
// After waiting for the current loading operation to complete, check again
// if _loaded is true and OnlyIfNeeded is true, return early
if (OnlyIfNeeded && SharedService._loaded) {
return;
}
await SharedService.handleDataLoading();
// Mark as loaded
SharedService._loaded = true;
}
finally {
// Ensure we always reset the loading flag
SharedService.isLoading$.next(false);
}
}
static async handleDataLoading() {
const md = new Metadata();
// make sure startup is done
await StartupManager.Instance.Startup();
this._resourceTypes = ResourcePermissionEngine.Instance.ResourceTypes;
await SharedService.RefreshUserNotifications();
}
FormatColumnValue(col, value, maxLength = 0, trailingChars = "...") {
if (value === null || value === undefined)
return value;
try {
const retVal = col.EntityField.FormatValue(value, 0);
if (maxLength > 0 && retVal && retVal.length > maxLength)
return retVal.substring(0, maxLength) + trailingChars;
else
return retVal;
}
catch (e) {
LogError(e);
return value;
}
}
ConvertMarkdownStringToHtmlList(listType, text) {
return ConvertMarkdownStringToHtmlList(listType, text) ?? text;
}
InvokeManualResize(delay = 50) {
return InvokeManualResize(delay, this);
}
PushStatusUpdates() {
const gp = Metadata.Provider;
return gp.PushStatusUpdates();
}
_currentUserImage = '/assets/user.png';
get CurrentUserImage() {
return this._currentUserImage;
}
set CurrentUserImage(value) {
this._currentUserImage = value;
}
/**
* @deprecated Use MJNotificationService.UserNotifications instead
*/
static get UserNotifications() {
return MJNotificationService.UserNotifications;
}
/**
* @deprecated Use MJNotificationService.UnreadUserNotifications instead
*/
static get UnreadUserNotifications() {
return MJNotificationService.UnreadUserNotifications;
}
/**
* @deprecated Use MJNotificationService.UnreadUserNotificationCount instead
*/
static get UnreadUserNotificationCount() {
return MJNotificationService.UnreadUserNotificationCount;
}
/**
* Utility method that returns true if child is a descendant of parent, false otherwise.
*/
static IsDescendant(parent, child) {
if (parent && child && parent.nativeElement && child.nativeElement) {
let node = child.nativeElement.parentNode;
while (node != null) {
if (node == parent.nativeElement) {
return true;
}
node = node.parentNode;
}
}
return false;
}
/**
* Creates a notification in the database and refreshes the UI. Returns the notification object.
* @param title
* @param message
* @param resourceTypeId
* @param resourceRecordId
* @param resourceConfiguration Any object, it is converted to a string by JSON.stringify and stored in the database
* @returns
* @deprecated Use MJNotificationService.CreateNotification instead
*/
async CreateNotification(title, message, resourceTypeId, resourceRecordId, resourceConfiguration, displayToUser = true) {
return this.mjNotificationsService.CreateNotification(title, message, resourceTypeId, resourceRecordId, resourceConfiguration, displayToUser);
}
/**
* @deprecated Use MJNotificationService.RefreshUserNotifications instead
*/
static async RefreshUserNotifications() {
MJNotificationService.RefreshUserNotifications();
}
/**
* Creates a message that is not saved to the User Notifications table, but is displayed to the user.
* @param message - text to display
* @param style - display styling
* @param hideAfter - option to auto hide after the specified delay in milliseconds
* @deprecated Use MJNotificationService.CreateSimpleNotification instead
*/
CreateSimpleNotification(message, style = "success", hideAfter) {
return this.mjNotificationsService.CreateSimpleNotification(message, style, hideAfter);
}
_resourceTypeMap = [
{ routeSegment: 'record', name: 'records' },
{ routeSegment: 'view', name: 'user views' },
{ routeSegment: 'search', name: 'search results' },
{ routeSegment: 'report', name: 'reports' },
{ routeSegment: 'query', name: 'queries' },
{ routeSegment: 'dashboard', name: 'dashboards' },
{ routeSegment: 'list', name: 'lists' },
];
/**
* Maps a Resource Type record Name column to the corresponding route segment
* @param resourceTypeName
* @returns
*/
mapResourceTypeNameToRouteSegment(resourceTypeName) {
const item = this._resourceTypeMap.find(rt => rt.name.trim().toLowerCase() === resourceTypeName.trim().toLowerCase());
if (item)
return item.routeSegment;
else
return null;
}
/**
* Maps a route segment to the corresponding Resource Type record Name column
* @param resourceRouteSegment
* @returns
*/
mapResourceTypeRouteSegmentToName(resourceRouteSegment) {
const item = this._resourceTypeMap.find(rt => rt.routeSegment.trim().toLowerCase() === resourceRouteSegment.trim().toLowerCase());
if (item)
return item.name;
else
return null;
}
/**
* Opens an entity record in a new or existing tab
* Uses the modern NavigationService for tab-based navigation
*/
OpenEntityRecord(entityName, recordPkey) {
try {
console.log('SharedService.OpenEntityRecord called:', entityName, recordPkey.ToURLSegment());
// Use NavigationService to open in new tab-based UX
this.navigationService.OpenEntityRecord(entityName, recordPkey);
}
catch (e) {
console.error('Error in OpenEntityRecord:', e);
LogError(e);
}
}
static ɵfac = function SharedService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || SharedService)(i0.ɵɵinject(i1.NotificationService), i0.ɵɵinject(i2.MJNotificationService), i0.ɵɵinject(i0.Injector)); };
static ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: SharedService, factory: SharedService.ɵfac, providedIn: 'root' });
}
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(SharedService, [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], () => [{ type: i1.NotificationService }, { type: i2.MJNotificationService }, { type: i0.Injector }], null); })();
export const HtmlListType = {
Unordered: 'Unordered',
Ordered: 'Ordered',
};
export const EventCodes = {
ViewClicked: "ViewClicked",
EntityRecordClicked: "EntityRecordClicked",
AddDashboard: "AddDashboard",
AddReport: "AddReport",
AddQuery: "AddQuery",
ViewCreated: "ViewCreated",
ViewUpdated: "ViewUpdated",
RunSearch: "RunSearch",
ViewNotifications: "ViewNotifications",
PushStatusUpdates: "PushStatusUpdates",
UserNotificationsUpdated: "UserNotificationsUpdated",
CloseCurrentTab: "CloseCurrentTab",
ListCreated: "ListCreated",
ListClicked: 'ListClicked',
AvatarUpdated: 'AvatarUpdated'
};
//# sourceMappingURL=shared.service.js.map