UNPKG

@fleetbase/ember-core

Version:

Provides all the core services, decorators and utilities for building a Fleetbase extension for the Console.

872 lines (792 loc) 29.9 kB
import Service from '@ember/service'; import Evented from '@ember/object/evented'; import { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { getOwner } from '@ember/application'; import { A } from '@ember/array'; import MenuItem from '../contracts/menu-item'; /** * UniverseService (Refactored) * * This is the new UniverseService that acts as a facade to the specialized sub-services. * It maintains backward compatibility with the old API while delegating to the new architecture. * * The service decomposition provides: * - ExtensionManager: Handles lazy loading of engines * - RegistryService: Manages all registries using Ember's container * - MenuService: Manages menu items and panels * - WidgetService: Manages dashboard widgets * - HookService: Manages application hooks * * @class UniverseService * @extends Service */ export default class UniverseService extends Service.extend(Evented) { // Inject specialized services @service('universe/extension-manager') extensionManager; @service('universe/registry-service') registryService; @service('universe/menu-service') menuService; @service('universe/widget-service') widgetService; @service('universe/hook-service') hookService; @service router; @service intl; @service urlSearchParams; @tracked applicationInstance; @tracked initialLocation = { ...window.location }; @tracked bootCallbacks = A([]); /** * Set the application instance on this service and cascade to RegistryService * Called by the instance initializer to ensure both services have access * to the root application container * * @method setApplicationInstance * @param {Application} application The root application instance */ setApplicationInstance(application) { this.applicationInstance = application; // Cascade to all child services if (this.registryService) { this.registryService.setApplicationInstance(application); } if (this.extensionManager) { this.extensionManager.setApplicationInstance(application); } if (this.menuService) { this.menuService.setApplicationInstance(application); } if (this.widgetService) { this.widgetService.setApplicationInstance(application); } if (this.hookService) { this.hookService.setApplicationInstance(application); } } /** * Get a service by name * Convenience method for extensions to access specialized services * * Supports multiple naming patterns: * - "universe/menu-service" -> universe/menu-service * - "menu-service" -> universe/menu-service * - "menuService" -> universe/menu-service * - "menu" -> universe/menu-service * - "hooks" or "hook" -> universe/hook-service * - "widgets" or "widget" -> universe/widget-service * - "registry" -> universe/registry-service * * @method getService * @param {String} serviceName Service name in various formats * @returns {Service} The service instance */ getService(serviceName) { const owner = getOwner(this); let resolvedName = serviceName; // Normalize the service name if (!/\//.test(serviceName)) { // No slash, might be camelCase or short name const kebabCase = serviceName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); // Map short names and variations to full service names const nameMapping = { hook: 'hook-service', hooks: 'hook-service', 'hook-service': 'hook-service', menu: 'menu-service', 'menu-service': 'menu-service', widget: 'widget-service', widgets: 'widget-service', 'widget-service': 'widget-service', registry: 'registry-service', 'registry-service': 'registry-service', }; const mappedName = nameMapping[kebabCase] || kebabCase; resolvedName = `universe/${mappedName}`; } else if (serviceName.startsWith('universe/')) { // Already has universe/ prefix, ensure it's using -service naming resolvedName = serviceName; } return owner.lookup(`service:${resolvedName}`); } // ============================================================================ // Extension Management (delegates to ExtensionManager) // ============================================================================ /** * Ensure an engine is loaded * * @method ensureEngineLoaded * @param {String} engineName Engine name * @returns {Promise<EngineInstance>} Engine instance */ async ensureEngineLoaded(engineName) { return this.extensionManager.ensureEngineLoaded(engineName); } /** * Get an engine instance * * @method getEngineInstance * @param {String} engineName Engine name * @returns {EngineInstance|null} Engine instance or null */ getEngineInstance(engineName) { return this.extensionManager.getEngineInstance(engineName); } /** * Register an extension * * @method registerExtension * @param {String} name Extension name * @param {Object} metadata Extension metadata */ registerExtension(name, metadata = {}) { this.extensionManager.registerExtension(name, metadata); } /** * Listen for a specific engine to be loaded * Note: This uses event listeners and will NOT run if the engine is already loaded. * Use whenEngineLoaded() if you want to handle both cases. * * @method onEngineLoaded * @param {String} engineName The engine name to listen for * @param {Function} callback Function to call when the engine loads, receives engineInstance as parameter * @example * universe.onEngineLoaded('@fleetbase/fleetops-engine', (engineInstance) => { * console.log('FleetOps engine loaded!', engineInstance); * }); */ onEngineLoaded(engineName, callback) { this.extensionManager.on('engine.loaded', (name, instance) => { if (name === engineName) { callback(instance); } }); } /** * Execute a callback when an engine is loaded * If the engine is already loaded, the callback runs immediately * Otherwise, it's stored and runs when the engine loads * * This is the recommended way to handle engine-dependent setup. * * @method whenEngineLoaded * @param {String} engineName The engine name * @param {Function} callback Function to call, receives (engineInstance, universe, appInstance) * @example * // Replaces this pattern: * if (universe.extensionManager.isEngineLoaded('@fleetbase/fleetops-engine')) { * const engine = universe.extensionManager.getEngineInstance('@fleetbase/fleetops-engine'); * doSomething(engine); * } else { * universe.onEngineLoaded('@fleetbase/fleetops-engine', (engine) => { * doSomething(engine); * }); * } * * // With this simpler pattern: * universe.whenEngineLoaded('@fleetbase/fleetops-engine', (engine) => { * doSomething(engine); * }); */ whenEngineLoaded(engineName, callback) { return this.extensionManager.whenEngineLoaded(engineName, callback); } /** * Get the application instance * * @method getApplicationInstance * @returns {ApplicationInstance} The application instance */ getApplicationInstance() { return this.applicationInstance; } /** * Get a service from a specific engine * * @method getServiceFromEngine * @param {String} engineName The engine name * @param {String} serviceName The service name * @param {Object} options Optional options * @param {Object} options.inject Properties to inject into the service * @returns {Service|null} The service instance or null * @example * const userService = universe.getServiceFromEngine('user-engine', 'user'); * if (userService) { * userService.doSomething(); * } */ getServiceFromEngine(engineName, serviceName, options = {}) { const engineInstance = this.getEngineInstance(engineName); if (engineInstance && typeof serviceName === 'string') { const serviceInstance = engineInstance.lookup(`service:${serviceName}`); if (options && options.inject) { for (let injectionName in options.inject) { serviceInstance[injectionName] = options.inject[injectionName]; } } return serviceInstance; } return null; } // ============================================================================ // Registry Management (delegates to RegistryService) // ============================================================================ /** * Create a new registry * * @method createRegistry * @param {String} name Registry name * @returns {Array} The created registry */ createRegistry(name) { return this.registryService.createRegistry(name); } /** * Create multiple registries * * @method createRegistries * @param {Array} names Array of registry names */ createRegistries(names) { this.registryService.createRegistries(names); } /** * Get a registry * * @method getRegistry * @param {String} name Registry name * @returns {Array} Registry items */ getRegistry(name) { return this.registryService.getRegistry(name); } /** * Register an item to a registry * * @method registerInRegistry * @param {String} registryName Registry name * @param {String} key Item key * @param {*} value Item value */ registerInRegistry(registryName, key, value) { this.registryService.register(registryName, key, value); } /** * Lookup an item from a registry * * @method lookupFromRegistry * @param {String} registryName Registry name * @param {String} key Item key * @returns {*} The registered item */ lookupFromRegistry(registryName, key) { return this.registryService.lookup(registryName, key); } // ============================================================================ // Application Container Registration (delegates to RegistryService) // ============================================================================ /** * Registers a component to the root application container. * This ensures the component is available to all engines and the host app. * @method registerComponent * @param {String} name The component name (e.g., 'my-component') * @param {Class} componentClass The component class * @param {Object} options Registration options */ registerComponent(name, componentClass, options = {}) { this.registryService.registerComponent(name, componentClass, options); } /** * Registers a service to the root application container. * This ensures the service is available to all engines and the host app. * @method registerService * @param {String} name The service name (e.g., 'my-service') * @param {Class} serviceClass The service class * @param {Object} options Registration options */ registerService(name, serviceClass, options = {}) { this.registryService.registerService(name, serviceClass, options); } // ============================================================================ // Menu Management (delegates to MenuService) // ============================================================================ /** * Register a header menu item * * @method registerHeaderMenuItem * @param {MenuItem|String} menuItemOrTitle MenuItem instance or title * @param {String} route Optional route * @param {Object} options Optional options */ registerHeaderMenuItem(menuItemOrTitle, route = null, options = {}) { this.menuService.registerHeaderMenuItem(menuItemOrTitle, route, options); } /** * Register an organization menu item * * @method registerOrganizationMenuItem * @param {MenuItem|String} menuItemOrTitle MenuItem instance or title * @param {Object} options Optional options */ registerOrganizationMenuItem(menuItemOrTitle, options = {}) { this.menuService.registerOrganizationMenuItem(menuItemOrTitle, options); } /** * Register a user menu item * * @method registerUserMenuItem * @param {MenuItem|String} menuItemOrTitle MenuItem instance or title * @param {Object} options Optional options */ registerUserMenuItem(menuItemOrTitle, options = {}) { this.menuService.registerUserMenuItem(menuItemOrTitle, options); } /** * Register an admin menu panel * * @method registerAdminMenuPanel * @param {MenuPanel|String} panelOrTitle MenuPanel instance or title * @param {Array} items Optional items * @param {Object} options Optional options */ registerAdminMenuPanel(panelOrTitle, items = [], options = {}) { this.menuService.registerAdminMenuPanel(panelOrTitle, items, options); } /** * Register a settings menu item * * @method registerSettingsMenuItem * @param {MenuItem|String} menuItemOrTitle MenuItem instance or title * @param {Object} options Optional options */ registerSettingsMenuItem(menuItemOrTitle, options = {}) { this.menuService.registerSettingsMenuItem(menuItemOrTitle, options); } /** * Register a menu item to a custom registry * * @method registerMenuItem * @param {String} registryName Registry name * @param {MenuItem|String} menuItemOrTitle MenuItem instance or title * @param {String|Object} routeOrOptions Route or options * @param {Object} options Optional options */ registerMenuItem(registryName, menuItemOrTitle, routeOrOptions = {}, options = {}) { this.menuService.registerMenuItem(registryName, menuItemOrTitle, routeOrOptions, options); } /** * Get header menu items * * @computed headerMenuItems * @returns {Array} Header menu items */ get headerMenuItems() { return this.menuService.getHeaderMenuItems(); } /** * Get organization menu items * * @computed organizationMenuItems * @returns {Array} Organization menu items */ get organizationMenuItems() { return this.menuService.getOrganizationMenuItems(); } /** * Get user menu items * * @computed userMenuItems * @returns {Array} User menu items */ get userMenuItems() { return this.menuService.getUserMenuItems(); } /** * Get admin menu items * * @computed adminMenuItems * @returns {Array} Admin menu items */ get adminMenuItems() { return this.menuService.getAdminMenuItems(); } /** * Get admin menu panels * * @computed adminMenuPanels * @returns {Array} Admin menu panels */ get adminMenuPanels() { return this.menuService.getAdminMenuPanels(); } // ============================================================================ // Widget Management (delegates to WidgetService) // ============================================================================ /** * Register default dashboard widgets * * @method registerDefaultDashboardWidgets * @param {Array<Widget>} widgets Array of widgets */ registerDefaultDashboardWidgets(widgets) { this.widgetService.registerDefaultDashboardWidgets(widgets); } /** * Register dashboard widgets * * @method registerDashboardWidgets * @param {Array<Widget>} widgets Array of widgets */ registerDashboardWidgets(widgets) { this.widgetService.registerDashboardWidgets(widgets); } /** * Register a dashboard * * @method registerDashboard * @param {String} name Dashboard name * @param {Object} options Dashboard options */ registerDashboard(name, options = {}) { this.widgetService.registerDashboard(name, options); } /** * Register a dashboard render slot. * * @method registerDashboardSlot * @param {String} slotId Dashboard slot identifier * @param {Object} options Slot options */ registerDashboardSlot(slotId, options = {}) { this.widgetService.registerDashboardSlot(slotId, options); } /** * Register a system dashboard for a render slot. * * @method registerDashboardForSlot * @param {String} slotId Dashboard slot identifier * @param {String} dashboardName Dashboard widget namespace * @param {Object} options Dashboard slot options */ registerDashboardForSlot(slotId, dashboardName, options = {}) { this.widgetService.registerDashboardForSlot(slotId, dashboardName, options); } /** * Set the first system dashboard for a render slot. * * @method setDefaultDashboardForSlot * @param {String} slotId Dashboard slot identifier * @param {String} dashboardName Dashboard widget namespace */ setDefaultDashboardForSlot(slotId, dashboardName) { this.widgetService.setDefaultDashboardForSlot(slotId, dashboardName); } /** * Set the dashboard that should load first on the console home. * * @method setConsoleDashboard * @param {String} dashboardName Dashboard widget namespace */ setConsoleDashboard(dashboardName) { this.widgetService.setConsoleDashboard(dashboardName); } /** * Get dashboard widgets * * @computed dashboardWidgets * @returns {Object} Dashboard widgets object */ get dashboardWidgets() { return { defaultWidgets: this.widgetService.getDefaultWidgets(), widgets: this.widgetService.getWidgets(), }; } // ============================================================================ // Hook Management (delegates to HookService) // ============================================================================ /** * Register a hook * * @method registerHook * @param {Hook|String} hookOrName Hook instance or name * @param {Function} handler Optional handler * @param {Object} options Optional options */ registerHook(hookOrName, handler = null, options = {}) { this.hookService.registerHook(hookOrName, handler, options); } /** * Execute hooks * * @method executeHook * @param {String} hookName Hook name * @param {...*} args Arguments to pass to hooks * @returns {Promise<Array>} Array of hook results */ async executeHook(hookName, ...args) { return this.hookService.execute(hookName, ...args); } /** * Get hooks * * @computed hooks * @returns {Object} Hooks object */ get hooks() { return this.hookService.hooks; } // ============================================================================ // Utility Methods // ============================================================================ /** * Get view from transition * * @method getViewFromTransition * @param {Object} transition Transition object * @returns {String|null} View parameter */ getViewFromTransition(transition) { const queryParams = transition.to?.queryParams ?? { view: null }; return queryParams.view; } /** * Virtual route redirect * Handles redirecting to menu items based on URL slug * * @method virtualRouteRedirect * @param {Object} transition Transition object * @param {String} registryName Registry name * @param {String} route Route name * @param {Object} options Options * @returns {Promise} Transition promise */ async virtualRouteRedirect(transition, registryName, route, options = {}) { const view = this.getViewFromTransition(transition); const slug = window.location.pathname.replace('/', ''); const queryParams = this.urlSearchParams.all(); const menuItem = this.lookupMenuItemFromRegistry(registryName, slug, view); if (menuItem && transition.from === null) { return this.transitionMenuItem(route, menuItem, { queryParams }).then((transition) => { if (options && options.restoreQueryParams === true) { this.urlSearchParams.setParamsToCurrentUrl(queryParams); } return transition; }); } } /** * Transition to a menu item * Handles section, slug, and view parameters for virtual routes * * @method transitionMenuItem * @param {String} route Route name * @param {Object} menuItem Menu item object with slug, view, and optional section * @returns {Transition} The router transition */ @action transitionMenuItem(route, menuItem) { const { slug, view, section } = menuItem; if (section && slug && view) { return this.router.transitionTo(route, section, slug, { queryParams: { view } }); } if (section && slug) { return this.router.transitionTo(route, section, slug); } if (slug && view) { return this.router.transitionTo(route, slug, { queryParams: { view } }); } return this.router.transitionTo(route, slug); } /** * Register a boot callback * * @method onBoot * @param {Function} callback Callback function */ onBoot(callback) { if (typeof callback === 'function') { this.bootCallbacks.pushObject(callback); } } /** * Execute boot callbacks * * @method executeBootCallbacks */ async executeBootCallbacks() { for (const callback of this.bootCallbacks) { try { await callback(this); } catch (error) { console.error('Error executing boot callback:', error); } } // Mark boot as complete this.extensionManager.finishBoot(); } // ============================================================================ // Backward Compatibility Methods // ============================================================================ /** * Get menu items from a registry * Backward compatibility facade * * @method getMenuItemsFromRegistry * @param {String} registryName Registry name * @returns {Array} Menu items */ getMenuItemsFromRegistry(registryName) { return this.registryService.getRegistry(registryName) || A([]); } /** * Get menu panels from a registry * Backward compatibility facade * * @method getMenuPanelsFromRegistry * @param {String} registryName Registry name * @returns {Array} Menu panels */ getMenuPanelsFromRegistry(registryName) { return this.registryService.getRegistry(`${registryName}:panels`) || A([]); } /** * Lookup a menu item from a registry * Backward compatibility facade * * @method lookupMenuItemFromRegistry * @param {String} registryName Registry name * @param {String} slug Menu item slug * @param {String} view Optional view * @param {String} section Optional section * @returns {Object|null} Menu item or null */ lookupMenuItemFromRegistry(registryName, slug, view = null, section = null) { const items = this.getMenuItemsFromRegistry(registryName); return items.find((item) => { const slugMatch = item.slug === slug; const viewMatch = !view || item.view === view; const sectionMatch = !section || item.section === section; return slugMatch && viewMatch && sectionMatch; }); } /** * Create a registry event * Backward compatibility facade * * @method createRegistryEvent * @param {String} registryName Registry name * @param {String} eventName Event name * @param {...*} args Event arguments */ createRegistryEvent(registryName, eventName, ...args) { this.trigger(`${registryName}:${eventName}`, ...args); } /** * Register after boot callback * Backward compatibility facade * * @method afterBoot * @param {Function} callback Callback function */ afterBoot(callback) { this.extensionManager.afterBoot(callback); } /** * Create a menu item (internal helper) * Backward compatibility helper * * @method _createMenuItem * @param {String} title Menu item title * @param {String} route Menu item route * @param {Object} options Menu item options * @returns {Object} Menu item object */ _createMenuItem(title, route = null, options = {}) { const menuItem = new MenuItem(title, route); if (options.icon) menuItem.withIcon(options.icon); if (options.component) menuItem.withComponent(options.component); if (options.slug) menuItem.withSlug(options.slug); if (options.section) menuItem.inSection(options.section); if (options.priority) menuItem.withPriority(options.priority); if (options.type) menuItem.withType(options.type); if (options.wrapperClass) menuItem.withWrapperClass(options.wrapperClass); if (options.queryParams) menuItem.withQueryParams(options.queryParams); if (options.onClick) menuItem.onClick(options.onClick); return menuItem.toObject(); } /** * Register a renderable component for cross-engine rendering * Facade method - delegates to RegistryService * * @method registerRenderableComponent * @param {String} registryName Registry name (slot identifier) * @param {Object|Class|Array} component ExtensionComponent definition, component class, or array of either * @param {Object} options Optional configuration * * @example * // ExtensionComponent definition with path (lazy loading) * universe.registerRenderableComponent( * 'fleet-ops:component:order:details', * new ExtensionComponent('@fleetbase/storefront-engine', 'storefront-order-summary') * ); */ registerRenderableComponent(registryName, component, options = {}) { return this.registryService.registerRenderableComponent(registryName, component, options); } /** * Get renderable components from a registry * Backward compatibility method - delegates to RegistryService * * @method getRenderableComponentsFromRegistry * @param {String} registryName Registry name * @returns {Array} Array of component definitions/classes */ getRenderableComponentsFromRegistry(registryName) { return this.registryService.getRenderableComponents(registryName); } /** * Register a helper to the application container * Makes the helper available globally to all engines and the host app * Facade method - delegates to RegistryService * * @method registerHelper * @param {String} helperName The helper name (e.g., 'calculate-delivery-fee') * @param {Function|Class|TemplateHelper} helperClassOrTemplateHelper Helper function, class, or TemplateHelper instance * @param {Object} options Registration options * @returns {Promise<void>} * * @example * // Direct function registration * await universe.registerHelper('calculate-delivery-fee', calculateDeliveryFeeHelper); * * @example * // Lazy loading from engine (ensures engine is loaded first) * import TemplateHelper from '@fleetbase/ember-core/contracts/template-helper'; * await universe.registerHelper( * 'calculate-delivery-fee', * new TemplateHelper('@fleetbase/storefront-engine', 'helpers/calculate-delivery-fee') * ); */ async registerHelper(helperName, helperClassOrTemplateHelper, options = {}) { return await this.registryService.registerHelper(helperName, helperClassOrTemplateHelper, options); } /** * Legacy method for registering components in engines * Maintained for backward compatibility * * @method registerComponentInEngine * @param {String} engineName Engine name * @param {*} componentClass Component class * @param {Object} options Options */ async registerComponentInEngine(engineName, componentClass, options = {}) { const engineInstance = await this.ensureEngineLoaded(engineName); if (engineInstance && componentClass && typeof componentClass.name === 'string') { const dasherized = componentClass.name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); engineInstance.register(`component:${componentClass.name}`, componentClass); engineInstance.register(`component:${dasherized}`, componentClass); if (options.registerAs) { engineInstance.register(`component:${options.registerAs}`, componentClass); } } } }