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.

325 lines 14 kB
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