From 9d02322f020df3b770aa6f6b64edabf69cf4458e Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Tue, 10 Sep 2024 16:21:29 +0800 Subject: [PATCH] improvements and fixes to core services --- addon/services/session.js | 45 +- addon/services/theme.js | 2 +- addon/services/universe.js | 741 ++++++++++++++---- addon/utils/is-empty-object.js | 10 +- .../normalize-relations-with-hash.js | 7 +- package.json | 2 +- 6 files changed, 610 insertions(+), 197 deletions(-) diff --git a/addon/services/session.js b/addon/services/session.js index 4abd23a..b967615 100644 --- a/addon/services/session.js +++ b/addon/services/session.js @@ -2,6 +2,7 @@ import SimpleAuthSessionService from 'ember-simple-auth/services/session'; import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; import { later } from '@ember/runloop'; +import { debug } from '@ember/debug'; import getWithDefault from '../utils/get-with-default'; export default class SessionService extends SimpleAuthSessionService { @@ -28,7 +29,7 @@ export default class SessionService extends SimpleAuthSessionService { * * @return {SessionService} */ - isOnboarding() { + isOnboarding () { this._isOnboarding = true; return this; @@ -37,7 +38,7 @@ export default class SessionService extends SimpleAuthSessionService { /** * Manually authenticate user */ - manuallyAuthenticate(authToken) { + manuallyAuthenticate (authToken) { return this.session._setup('authenticator:fleetbase', { token: authToken }, true); } @@ -46,16 +47,13 @@ export default class SessionService extends SimpleAuthSessionService { * * @void */ - async handleAuthentication() { + async handleAuthentication () { if (this._isOnboarding) { return; } const loaderNode = this.showLoader('Starting session...'); - this.isLoaderNodeOpen = true; - - try { - await this.router.transitionTo(this.redirectTo); + const removeLoaderNode = () => { later( this, () => { @@ -63,11 +61,18 @@ export default class SessionService extends SimpleAuthSessionService { document.body.removeChild(loaderNode); this.isLoaderNodeOpen = false; }, - 600 * 6 + 600 * 3 ); + }; + this.isLoaderNodeOpen = true; + + try { + await this.router.transitionTo(this.redirectTo); } catch (error) { - this.notifications.serverError(error); + debug(`Session's handleAuthentication() failed to transition: ${error.message}`); } + + removeLoaderNode(); } /** @@ -75,7 +80,7 @@ export default class SessionService extends SimpleAuthSessionService { * * @void */ - async loadCurrentUser() { + async loadCurrentUser () { try { const user = await this.currentUser.load(); @@ -95,7 +100,7 @@ export default class SessionService extends SimpleAuthSessionService { * @param {Transition} transition * @void */ - async promiseCurrentUser(transition = null) { + async promiseCurrentUser (transition = null) { const invalidateWithLoader = this.invalidateWithLoader.bind(this); try { @@ -114,6 +119,7 @@ export default class SessionService extends SimpleAuthSessionService { if (transition) { transition.abort(); } + await invalidateWithLoader(error.message ?? 'Session authentication failed...'); throw error; } @@ -125,7 +131,7 @@ export default class SessionService extends SimpleAuthSessionService { * @param {String} loadingMessage * @return {HTMLElement} loader */ - showLoader(loadingMessage) { + showLoader (loadingMessage) { const loader = document.createElement('div'); loader.classList.add('overloader'); loader.innerHTML = `
@@ -147,15 +153,14 @@ export default class SessionService extends SimpleAuthSessionService { * @param {String} loadingMessage * @return {Promise} */ - invalidateWithLoader(loadingMessage = 'Ending session...') { + invalidateWithLoader (loadingMessage = 'Ending session...') { // if loader node is open already just invalidate if (this.isLoaderNodeOpen === true) { return this.session.invalidate(); } const loaderNode = this.showLoader(loadingMessage); - - this.isLoaderNodeOpen = false; + this.isLoaderNodeOpen = true; return this.session.invalidate().then(() => { later( @@ -174,7 +179,7 @@ export default class SessionService extends SimpleAuthSessionService { * * @void */ - setRedirect(whereTo = 'console') { + setRedirect (whereTo = 'console') { this.redirectTo = whereTo; } @@ -183,7 +188,7 @@ export default class SessionService extends SimpleAuthSessionService { * * @return {Date} */ - getExpiresAtDate() { + getExpiresAtDate () { return new Date(this.data.authenticated.expires_at); } @@ -192,7 +197,7 @@ export default class SessionService extends SimpleAuthSessionService { * * @return {Integer} */ - getSessionSecondsRemaining() { + getSessionSecondsRemaining () { const date = this.getExpiresAtDate(); const now = new Date(); @@ -206,8 +211,8 @@ export default class SessionService extends SimpleAuthSessionService { * @return {Promise} * @throws {Error} */ - checkForTwoFactor(identity) { - return this.fetch.get('two-fa/check', { identity }).catch((error) => { + checkForTwoFactor (identity) { + return this.fetch.get('two-fa/check', { identity }).catch(error => { throw new Error(error.message); }); } diff --git a/addon/services/theme.js b/addon/services/theme.js index b14d76b..7ac246d 100644 --- a/addon/services/theme.js +++ b/addon/services/theme.js @@ -15,7 +15,7 @@ export default class ThemeService extends Service { */ get router() { const owner = getOwner(this); - const router = owner.lookup('service:router'); + const router = owner.lookup('router:main'); return router; } diff --git a/addon/services/universe.js b/addon/services/universe.js index 0b49281..ab2a4f8 100644 --- a/addon/services/universe.js +++ b/addon/services/universe.js @@ -7,8 +7,9 @@ import { isBlank } from '@ember/utils'; import { A, isArray } from '@ember/array'; import { later } from '@ember/runloop'; import { dasherize, camelize } from '@ember/string'; +import { pluralize } from 'ember-inflector'; import { getOwner } from '@ember/application'; -import { assert, debug } from '@ember/debug'; +import { assert, debug, warn } from '@ember/debug'; import RSVP from 'rsvp'; import loadInstalledExtensions from '../utils/load-installed-extensions'; import loadExtensions from '../utils/load-extensions'; @@ -18,18 +19,21 @@ import config from 'ember-get-config'; export default class UniverseService extends Service.extend(Evented) { @service router; @service intl; + @tracked applicationInstance; + @tracked enginesBooted = false; + @tracked bootedExtensions = A([]); @tracked headerMenuItems = A([]); @tracked organizationMenuItems = A([]); @tracked userMenuItems = A([]); - @tracked adminRegistry = { + @tracked consoleAdminRegistry = { menuItems: A([]), menuPanels: A([]), }; - @tracked accountRegistry = { + @tracked consoleAccountRegistry = { menuItems: A([]), menuPanels: A([]), }; - @tracked settingsRegistry = { + @tracked consoleSettingsRegistry = { menuItems: A([]), menuPanels: A([]), }; @@ -37,6 +41,7 @@ export default class UniverseService extends Service.extend(Evented) { defaultWidgets: A([]), widgets: A([]), }; + @tracked hooks = {}; /** * Computed property that returns all administrative menu items. @@ -47,8 +52,8 @@ export default class UniverseService extends Service.extend(Evented) { * @memberof UniverseService * @returns {Array} Array of administrative menu items */ - @computed('adminRegistry.menuItems.[]') get adminMenuItems() { - return this.adminRegistry.menuItems; + @computed('consoleAdminRegistry.menuItems.[]') get adminMenuItems () { + return this.consoleAdminRegistry.menuItems; } /** @@ -60,8 +65,8 @@ export default class UniverseService extends Service.extend(Evented) { * @memberof UniverseService * @returns {Array} Array of administrative menu panels */ - @computed('adminRegistry.menuPanels.[]') get adminMenuPanels() { - return this.adminRegistry.menuPanels; + @computed('consoleAdminRegistry.menuPanels.[]') get adminMenuPanels () { + return this.consoleAdminRegistry.menuPanels; } /** @@ -73,8 +78,8 @@ export default class UniverseService extends Service.extend(Evented) { * @memberof UniverseService * @returns {Array} Array of administrative menu items */ - @computed('settingsRegistry.menuItems.[]') get settingsMenuItems() { - return this.settingsRegistry.menuItems; + @computed('consoleSettingsRegistry.menuItems.[]') get settingsMenuItems () { + return this.consoleSettingsRegistry.menuItems; } /** @@ -86,8 +91,8 @@ export default class UniverseService extends Service.extend(Evented) { * @memberof UniverseService * @returns {Array} Array of administrative menu panels */ - @computed('settingsRegistry.menuPanels.[]') get settingsMenuPanels() { - return this.settingsRegistry.menuPanels; + @computed('consoleSettingsRegistry.menuPanels.[]') get settingsMenuPanels () { + return this.consoleSettingsRegistry.menuPanels; } /** @@ -106,7 +111,7 @@ export default class UniverseService extends Service.extend(Evented) { * // Transitions to the 'management.fleets.index.new' route within the '@fleetbase/fleet-ops' engine. * this.transitionToEngineRoute('@fleetbase/fleet-ops', 'management.fleets.index.new'); */ - @action transitionToEngineRoute(engineName, route, ...args) { + @action transitionToEngineRoute (engineName, route, ...args) { const engineInstance = this.getEngineInstance(engineName); if (engineInstance) { @@ -130,13 +135,22 @@ export default class UniverseService extends Service.extend(Evented) { return this.router.transitionTo(route, ...args); } + /** + * Retrieves the application instance. + * + * @returns {ApplicationInstance} - The application instance object. + */ + getApplicationInstance () { + return this.applicationInstance; + } + /** * Retrieves the mount point of a specified engine by its name. * @param {string} engineName - The name of the engine for which to get the mount point. * @returns {string|null} The mount point of the engine or null if not found. */ - getEngineMountPoint(engineName) { + getEngineMountPoint (engineName) { const engineInstance = this.getEngineInstance(engineName); return this._getMountPointFromEngineInstance(engineInstance); } @@ -148,7 +162,7 @@ export default class UniverseService extends Service.extend(Evented) { * @returns {string|null} The resolved mount point or null if the instance is undefined or the configuration is not set. * @private */ - _getMountPointFromEngineInstance(engineInstance) { + _getMountPointFromEngineInstance (engineInstance) { if (engineInstance) { const config = engineInstance.resolveRegistration('config:environment'); @@ -184,7 +198,7 @@ export default class UniverseService extends Service.extend(Evented) { * // returns 'console.some' * _mountPathFromEngineName('@fleetbase/some-engine'); */ - _mountPathFromEngineName(engineName) { + _mountPathFromEngineName (engineName) { let engineNameSegments = engineName.split('/'); let mountName = engineNameSegments[1]; @@ -210,7 +224,7 @@ export default class UniverseService extends Service.extend(Evented) { * // To refresh the current route * this.refreshRoute(); */ - @action refreshRoute() { + @action refreshRoute () { return this.router.refresh(); } @@ -229,14 +243,22 @@ export default class UniverseService extends Service.extend(Evented) { * * @returns {Transition} Returns a Transition object representing the transition to the route. */ - @action transitionMenuItem(route, menuItem) { - const { slug, view } = menuItem; + @action transitionMenuItem (route, menuItem) { + const { slug, view, section } = menuItem; + + if (section && slug && view) { + return this.router.transitionTo(route, section, slug, { queryParams: { view } }); + } - if (view) { - return this.router.transitionTo(route, slug, view); + if (section && slug) { + return this.router.transitionTo(route, section, slug); } - return this.router.transitionTo(route, slug, 'index'); + if (slug && view) { + return this.router.transitionTo(route, slug, { queryParams: { view } }); + } + + return this.router.transitionTo(route, slug); } /** @@ -256,16 +278,23 @@ export default class UniverseService extends Service.extend(Evented) { * @example * createRegistry('myRegistry', { menuItems: ['item1', 'item2'], menuPanel: ['panel1', 'panel2'] }); */ - @action createRegistry(registryName, options = {}) { + @action createRegistry (registryName, options = {}) { const internalRegistryName = this.createInternalRegistryName(registryName); - this[internalRegistryName] = { - name: registryName, - menuItems: [], - menuPanels: [], - renderableComponents: [], - ...options, - }; + if (this[internalRegistryName] == undefined) { + this[internalRegistryName] = { + name: registryName, + menuItems: [], + menuPanels: [], + renderableComponents: [], + ...options, + }; + } else { + this[internalRegistryName] = { + ...this[internalRegistryName], + ...options, + }; + } // trigger registry created event this.trigger('registry.created', this[internalRegistryName]); @@ -287,7 +316,7 @@ export default class UniverseService extends Service.extend(Evented) { * @action * @memberof YourComponentOrClassName */ - @action createRegistries(registries = []) { + @action createRegistries (registries = []) { if (!isArray(registries)) { throw new Error('`createRegistries()` method must take an array.'); } @@ -318,7 +347,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {string} event - The name of the event to trigger. * @param {...*} params - Additional parameters to pass to the event handler. */ - @action createRegistryEvent(registryName, event, ...params) { + @action createRegistryEvent (registryName, event, ...params) { this.trigger(`${registryName}.${event}`, ...params); } @@ -334,7 +363,7 @@ export default class UniverseService extends Service.extend(Evented) { * @example * const myRegistry = getRegistry('myRegistry'); */ - @action getRegistry(registryName) { + @action getRegistry (registryName) { const internalRegistryName = this.createInternalRegistryName(registryName); const registry = this[internalRegistryName]; @@ -362,7 +391,7 @@ export default class UniverseService extends Service.extend(Evented) { * // Handle the error or absence of the registry * }); */ - lookupRegistry(registryName) { + lookupRegistry (registryName) { const internalRegistryName = this.createInternalRegistryName(registryName); const registry = this[internalRegistryName]; @@ -397,7 +426,7 @@ export default class UniverseService extends Service.extend(Evented) { * @example * const items = getMenuItemsFromRegistry('myRegistry'); */ - @action getMenuItemsFromRegistry(registryName) { + @action getMenuItemsFromRegistry (registryName) { const internalRegistryName = this.createInternalRegistryName(registryName); const registry = this[internalRegistryName]; @@ -420,7 +449,7 @@ export default class UniverseService extends Service.extend(Evented) { * @example * const panels = getMenuPanelsFromRegistry('myRegistry'); */ - @action getMenuPanelsFromRegistry(registryName) { + @action getMenuPanelsFromRegistry (registryName) { const internalRegistryName = this.createInternalRegistryName(registryName); const registry = this[internalRegistryName]; @@ -440,7 +469,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {string} registryName - The name of the registry to retrieve components from. * @returns {Array} An array of renderable components from the specified registry, or an empty array if none found. */ - @action getRenderableComponentsFromRegistry(registryName) { + @action getRenderableComponentsFromRegistry (registryName) { const internalRegistryName = this.createInternalRegistryName(registryName); const registry = this[internalRegistryName]; @@ -460,11 +489,11 @@ export default class UniverseService extends Service.extend(Evented) { * * @returns {Promise} Returns a Promise that resolves with the component if it is found, or null. */ - loadComponentFromRegistry(registryName, slug, view = null) { + loadComponentFromRegistry (registryName, slug, view = null) { const internalRegistryName = this.createInternalRegistryName(registryName); const registry = this[internalRegistryName]; - return new Promise((resolve) => { + return new Promise(resolve => { let component = null; if (isBlank(registry)) { @@ -519,14 +548,15 @@ export default class UniverseService extends Service.extend(Evented) { * @param {string} registryName - The name of the registry where the menu item is located. * @param {string} slug - The slug of the menu item. * @param {string} [view=null] - The view of the menu item, if applicable. + * @param {string} [section=null] - The section of the menu item, if applicable. * * @returns {Promise} Returns a Promise that resolves with the menu item if it is found, or null. */ - lookupMenuItemFromRegistry(registryName, slug, view = null) { + lookupMenuItemFromRegistry (registryName, slug, view = null, section = null) { const internalRegistryName = this.createInternalRegistryName(registryName); const registry = this[internalRegistryName]; - return new Promise((resolve) => { + return new Promise(resolve => { let foundMenuItem = null; if (isBlank(registry)) { @@ -537,8 +567,7 @@ export default class UniverseService extends Service.extend(Evented) { for (let i = 0; i < registry.menuItems.length; i++) { const menuItem = registry.menuItems[i]; - // no view hack - if (menuItem && menuItem.slug === slug && menuItem.view === null && view === 'index') { + if (menuItem && menuItem.slug === slug && menuItem.section === section && menuItem.view === view) { foundMenuItem = menuItem; break; } @@ -557,8 +586,7 @@ export default class UniverseService extends Service.extend(Evented) { for (let j = 0; j < menuPanel.items.length; j++) { const menuItem = menuPanel.items[j]; - // no view hack - if (menuItem && menuItem.slug === slug && menuItem.view === null && view === 'index') { + if (menuItem && menuItem.slug === slug && menuItem.section === section && menuItem.view === view) { foundMenuItem = menuItem; break; } @@ -575,6 +603,134 @@ export default class UniverseService extends Service.extend(Evented) { }); } + /** + * Gets the view param from the transition object. + * + * @param {Transition} transition + * @return {String|Null} + * @memberof UniverseService + */ + getViewFromTransition (transition) { + const queryParams = transition.to.queryParams ?? { view: null }; + return queryParams.view; + } + + /** + * Creates an internal registry name for hooks based on a given registry name. + * The registry name is transformed to camel case and appended with 'Hooks'. + * Non-alphanumeric characters are replaced with hyphens. + * + * @param {string} registryName - The name of the registry for which to create an internal hook registry name. + * @returns {string} - The internal hook registry name, formatted as camel case with 'Hooks' appended. + */ + createInternalHookRegistryName (registryName) { + return `${camelize(registryName.replace(/[^a-zA-Z0-9]/g, '-'))}Hooks`; + } + + /** + * Registers a hook function under a specified registry name. + * The hook is stored in an internal registry, and its hash is computed for identification. + * If the hook is already registered, it is appended to the existing list of hooks. + * + * @param {string} registryName - The name of the registry where the hook should be registered. + * @param {Function} hook - The hook function to be registered. + */ + registerHook (registryName, hook) { + if (typeof hook !== 'function') { + throw new Error('The hook must be a function.'); + } + + // no duplicate hooks + if (this.didRegisterHook(registryName, hook)) { + return; + } + + const internalHookRegistryName = this.createInternalHookRegistryName(registryName); + const hookRegistry = this.hooks[internalHookRegistryName] || []; + hookRegistry.pushObject({ id: this._createHashFromFunctionDefinition(hook), hook }); + + this.hooks[internalHookRegistryName] = hookRegistry; + } + + /** + * Checks if a hook was registered already. + * + * @param {String} registryName + * @param {Function} hook + * @return {Boolean} + * @memberof UniverseService + */ + didRegisterHook (registryName, hook) { + const hooks = this.getHooks(registryName); + const hookId = this._createHashFromFunctionDefinition(hook); + return isArray(hooks) && hooks.some(h => h.id === hookId); + } + + /** + * Retrieves the list of hooks registered under a specified registry name. + * If no hooks are registered, returns an empty array. + * + * @param {string} registryName - The name of the registry for which to retrieve hooks. + * @returns {Array} - An array of hook objects registered under the specified registry name. + * Each object contains an `id` and a `hook` function. + */ + getHooks (registryName) { + const internalHookRegistryName = this.createInternalHookRegistryName(registryName); + return this.hooks[internalHookRegistryName] ?? []; + } + + /** + * Executes all hooks registered under a specified registry name with the given parameters. + * Each hook is called with the provided parameters. + * + * @param {string} registryName - The name of the registry under which hooks should be executed. + * @param {...*} params - The parameters to pass to each hook function. + */ + executeHooks (registryName, ...params) { + const hooks = this.getHooks(registryName); + hooks.forEach(({ hook }) => { + try { + hook(...params); + } catch (error) { + debug(`Error executing hook: ${error}`); + } + }); + } + + /** + * Calls all hooks registered under a specified registry name with the given parameters. + * This is an alias for `executeHooks` for consistency in naming. + * + * @param {string} registryName - The name of the registry under which hooks should be called. + * @param {...*} params - The parameters to pass to each hook function. + */ + callHooks (registryName, ...params) { + this.executeHooks(registryName, ...params); + } + + /** + * Calls a specific hook identified by its ID under a specified registry name with the given parameters. + * Only the hook with the matching ID is executed. + * + * @param {string} registryName - The name of the registry where the hook is registered. + * @param {string} hookId - The unique identifier of the hook to be called. + * @param {...*} params - The parameters to pass to the hook function. + */ + callHook (registryName, hookId, ...params) { + const hooks = this.getHooks(registryName); + const hook = hooks.find(h => h.id === hookId); + + if (hook) { + try { + hook.hook(...params); + } catch (error) { + debug(`Error executing hook: ${error}`); + } + } else { + warn(`Hook with ID ${hookId} not found.`); + } + } + /** * Registers a renderable component or an array of components into a specified registry. * If a single component is provided, it is registered directly. @@ -585,9 +741,9 @@ export default class UniverseService extends Service.extend(Evented) { * @param {string} registryName - The registry name where the component(s) should be registered. * @param {Object|Array} component - The component or array of components to register. */ - registerRenderableComponent(engineName, registryName, component) { + registerRenderableComponent (engineName, registryName, component) { if (isArray(component)) { - component.forEach((_) => this.registerRenderableComponent(registryName, _)); + component.forEach(_ => this.registerRenderableComponent(registryName, _)); return; } @@ -619,7 +775,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {Array} items The items of the panel * @param {Object} options Additional options for the panel */ - registerMenuPanel(registryName, title, items = [], options = {}) { + registerMenuPanel (registryName, title, items = [], options = {}) { const internalRegistryName = this.createInternalRegistryName(registryName); const open = this._getOption(options, 'open', true); const slug = this._getOption(options, 'slug', dasherize(title)); @@ -652,7 +808,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {String} route The route of the item * @param {Object} options Additional options for the item */ - registerMenuItem(registryName, title, options = {}) { + registerMenuItem (registryName, title, options = {}) { const internalRegistryName = this.createInternalRegistryName(registryName); const route = this._getOption(options, 'route', `console.${dasherize(registryName)}.virtual`); options.slug = this._getOption(options, 'slug', '~'); @@ -684,6 +840,26 @@ export default class UniverseService extends Service.extend(Evented) { this.trigger('menuItem.registered', menuItem, this[internalRegistryName]); } + /** + * Register multiple menu items to a registry. + * + * @param {String} registryName + * @param {Array} [menuItems=[]] + * @memberof UniverseService + */ + registerMenuItems (registryName, menuItems = []) { + for (let i = 0; i < menuItems.length; i++) { + const menuItem = menuItems[i]; + if (menuItem && menuItem.title) { + if (menuItem.options) { + this.registerMenuItem(registryName, menuItem.title, menuItem.options); + } else { + this.registerMenuItem(registryName, menuItem.title, menuItem); + } + } + } + } + /** * Registers a menu item's component to one or multiple engines. * @@ -694,7 +870,7 @@ export default class UniverseService extends Service.extend(Evented) { * - `registerComponentToEngine`: A string or an array of strings representing the engine names where the component should be registered. * - `component`: The component class to register, which should have a 'name' property. */ - registerMenuItemComponentToEngine(options) { + registerMenuItemComponentToEngine (options) { // Register component if applicable if (typeof options.registerComponentToEngine === 'string') { this.registerComponentInEngine(options.registerComponentToEngine, options.component); @@ -722,9 +898,9 @@ export default class UniverseService extends Service.extend(Evented) { * @param {Array} items The items of the panel * @param {Object} options Additional options for the panel */ - registerAdminMenuPanel(title, items = [], options = {}) { + registerAdminMenuPanel (title, items = [], options = {}) { options.section = this._getOption(options, 'section', 'admin'); - this.registerMenuPanel('admin', title, items, options); + this.registerMenuPanel('console:admin', title, items, options); } /** @@ -736,8 +912,8 @@ export default class UniverseService extends Service.extend(Evented) { * @param {String} title The title of the item * @param {Object} options Additional options for the item */ - registerAdminMenuItem(title, options = {}) { - this.registerMenuItem('admin', title, options); + registerAdminMenuItem (title, options = {}) { + this.registerMenuItem('console:admin', title, options); } /** @@ -750,97 +926,255 @@ export default class UniverseService extends Service.extend(Evented) { * @param {Array} items The items of the panel * @param {Object} options Additional options for the panel */ - registerSettingsMenuPanel(title, items = [], options = {}) { - this.registerMenuPanel('settings', title, items, options); + registerSettingsMenuPanel (title, items = [], options = {}) { + this.registerMenuPanel('console:settings', title, items, options); } /** - * Registers a new dashboard widget in the universe service. + * Registers a new settings menu item. * - * @method registerDashboardWidgets + * @method registerSettingsMenuItem * @public * @memberof UniverseService - * @param {Object} widget - The widget object containing name, component, gridOptions, and options. - * @property {String} name - The name of the widget. - * @property {String} icon - The iron of the widget. - * @property {Function} component - The component associated with the widget. - * @property {Object} gridOptions - The grid options for the widget. - * @property {Object} options - Additional options for the widget. - */ - registerDashboardWidgets(widget) { - if (isArray(widget)) { - widget.forEach((w) => this.registerDashboardWidgets(w)); - return; - } - - const newWidget = this._createDashboardWidget(widget); - this.dashboardWidgets.widgets.pushObject(newWidget); - this.trigger('widget.registered', newWidget); + * @param {String} title The title of the item + * @param {Object} options Additional options for the item + */ + registerSettingsMenuItem (title, options = {}) { + this.registerMenuItem('console:settings', title, options); } /** - * Retrieves the widgets registered in the universe service. + * Registers a new account menu panel. * - * @method getDashboardWidgets + * @method registerAccountMenuPanel * @public * @memberof UniverseService - * @returns {Array} An array of registered widgets + * @param {String} title The title of the panel + * @param {Array} items The items of the panel + * @param {Object} options Additional options for the panel */ - getDashboardWidgets() { - return this.dashboardWidgets.widgets; + registerAccountMenuPanel (title, items = [], options = {}) { + this.registerMenuPanel('console:account', title, items, options); } /** - * Registers a new dashboard widget in the universe service. + * Registers a new account menu item. * - * @method registerDefaultDashboardWidgets + * @method registerAccountMenuItem * @public * @memberof UniverseService - * @param {Object} widget - The widget object containing name, component, gridOptions, and options. - * @property {String} name - The name of the widget. - * @property {String} icon - The iron of the widget. - * @property {Function} component - The component associated with the widget. - * @property {Object} gridOptions - The grid options for the widget. - * @property {Object} options - Additional options for the widget. - */ - registerDefaultDashboardWidgets(widget) { - if (isArray(widget)) { - widget.forEach((w) => this.registerDefaultDashboardWidgets(w)); + * @param {String} title The title of the item + * @param {Object} options Additional options for the item + */ + registerAccountMenuItem (title, options = {}) { + this.registerMenuItem('console:account', title, options); + } + + /** + * Registers a new dashboard with the given name. + * Initializes the dashboard with empty arrays for default widgets and widgets. + * + * @param {string} dashboardName - The name of the dashboard to register. + * @returns {void} + */ + registerDashboard (dashboardName) { + const internalDashboardRegistryName = this.createInternalDashboardName(dashboardName); + if (this[internalDashboardRegistryName] !== undefined) { return; } - const newWidget = this._createDashboardWidget(widget); - this.dashboardWidgets.defaultWidgets.pushObject(newWidget); - this.trigger('widget.registered', newWidget); + this[internalDashboardRegistryName] = { + defaultWidgets: A([]), + widgets: A([]), + }; + + this.trigger('dashboard.registered', this[internalDashboardRegistryName]); } /** - * Retrieves the widgets registered in the universe service. + * Retrieves the registry for a specific dashboard. * - * @method getDefaultDashboardWidgets - * @public + * @param {string} dashboardName - The name of the dashboard to get the registry for. + * @returns {Object} - The registry object for the specified dashboard, including default and registered widgets. + */ + getDashboardRegistry (dashboardName) { + const internalDashboardRegistryName = this.createInternalDashboardName(dashboardName); + return this[internalDashboardRegistryName]; + } + + /** + * Checks if a dashboard has been registered. + * + * @param {String} dashboardName + * @return {Boolean} * @memberof UniverseService - * @returns {Array} An array of registered widgets */ - getDefaultDashboardWidgets() { - return this.dashboardWidgets.defaultWidgets; + didRegisterDashboard (dashboardName) { + const internalDashboardRegistryName = this.createInternalDashboardName(dashboardName); + return this[internalDashboardRegistryName] !== undefined; + } + + /** + * Retrieves the widget registry for a specific dashboard and type. + * + * @param {string} dashboardName - The name of the dashboard to get the widget registry for. + * @param {string} [type='widgets'] - The type of widget registry to retrieve (e.g., 'widgets', 'defaultWidgets'). + * @returns {Array} - An array of widget objects for the specified dashboard and type. + */ + getWidgetRegistry (dashboardName, type = 'widgets') { + const internalDashboardRegistryName = this.createInternalDashboardName(dashboardName); + const typeKey = pluralize(type); + return isArray(this[internalDashboardRegistryName][typeKey]) ? this[internalDashboardRegistryName][typeKey] : []; } /** - * Creates a dashboard widget object from the given widget configuration. + * Registers widgets for a specific dashboard. + * Supports registering multiple widgets and different types of widget collections. * - * @param {Object} widget - The widget configuration object. - * @param {string} widget.widgetId - The unique identifier for the widget. + * @param {string} dashboardName - The name of the dashboard to register widgets for. + * @param {Array|Object} widgets - An array of widget objects or a single widget object to register. + * @param {string} [type='widgets'] - The type of widgets to register (e.g., 'widgets', 'defaultWidgets'). + * @returns {void} + */ + registerWidgets (dashboardName, widgets = [], type = 'widgets') { + const internalDashboardRegistryName = this.createInternalDashboardName(dashboardName); + if (isArray(widgets)) { + widgets.forEach(w => this.registerWidgets(dashboardName, w, type)); + return; + } + + const typeKey = pluralize(type); + const newWidget = this._createDashboardWidget(widgets); + const widgetRegistry = this.getWidgetRegistry(dashboardName, type); + if (this.widgetRegistryHasWidget(widgetRegistry, newWidget)) { + return; + } + + this[internalDashboardRegistryName][typeKey] = [...widgetRegistry, newWidget]; + this.trigger('widget.registered', newWidget); + } + + /** + * Checks if a widget with the same ID as the pending widget is already registered in the specified dashboard and type. + * + * @param {string} dashboardName - The name of the dashboard to check. + * @param {Object} widgetPendingRegistry - The widget to check for in the registry. + * @param {string} [type='widgets'] - The type of widget registry to check (e.g., 'widgets', 'defaultWidgets'). + * @returns {boolean} - `true` if a widget with the same ID is found in the registry; otherwise, `false`. + */ + didRegisterWidget (dashboardName, widgetPendingRegistry, type = 'widgets') { + const widgetRegistry = this.getWidgetRegistry(dashboardName, type); + return widgetRegistry.includes(widget => widget.widgetId === widgetPendingRegistry.widgetId); + } + + /** + * Checks if a widget with the same ID as the pending widget exists in the provided widget registry instance. + * + * @param {Array} [widgetRegistryInstance=[]] - An array of widget objects to check. + * @param {Object} widgetPendingRegistry - The widget to check for in the registry. + * @returns {boolean} - `true` if a widget with the same ID is found in the registry; otherwise, `false`. + */ + widgetRegistryHasWidget (widgetRegistryInstance = [], widgetPendingRegistry) { + return widgetRegistryInstance.includes(widget => widget.widgetId === widgetPendingRegistry.widgetId); + } + + /** + * Registers widgets for the default 'dashboard' dashboard. + * + * @param {Array} [widgets=[]] - An array of widget objects to register. + * @returns {void} + */ + registerDashboardWidgets (widgets = []) { + this.registerWidgets('dashboard', widgets); + } + + /** + * Registers default widgets for the default 'dashboard' dashboard. + * + * @param {Array} [widgets=[]] - An array of default widget objects to register. + * @returns {void} + */ + registerDefaultDashboardWidgets (widgets = []) { + this.registerWidgets('dashboard', widgets, 'defaultWidgets'); + } + + /** + * Registers default widgets for a specified dashboard. + * + * @param {String} dashboardName + * @param {Array} [widgets=[]] - An array of default widget objects to register. + * @returns {void} + */ + registerDefaultWidgets (dashboardName, widgets = []) { + this.registerWidgets(dashboardName, widgets, 'defaultWidgets'); + } + + /** + * Retrieves widgets for a specific dashboard. + * + * @param {string} dashboardName - The name of the dashboard to retrieve widgets for. + * @param {string} [type='widgets'] - The type of widgets to retrieve (e.g., 'widgets', 'defaultWidgets'). + * @returns {Array} - An array of widgets for the specified dashboard and type. + */ + getWidgets (dashboardName, type = 'widgets') { + const typeKey = pluralize(type); + const internalDashboardRegistryName = this.createInternalDashboardName(dashboardName); + return isArray(this[internalDashboardRegistryName][typeKey]) ? this[internalDashboardRegistryName][typeKey] : []; + } + + /** + * Retrieves default widgets for a specific dashboard. + * + * @param {string} dashboardName - The name of the dashboard to retrieve default widgets for. + * @returns {Array} - An array of default widgets for the specified dashboard. + */ + getDefaultWidgets (dashboardName) { + return this.getWidgets(dashboardName, 'defaultWidgets'); + } + + /** + * Retrieves widgets for the default 'dashboard' dashboard. + * + * @returns {Array} - An array of widgets for the default 'dashboard' dashboard. + */ + getDashboardWidgets () { + return this.getWidgets('dashboard'); + } + + /** + * Retrieves default widgets for the default 'dashboard' dashboard. + * + * @returns {Array} - An array of default widgets for the default 'dashboard' dashboard. + */ + getDefaultDashboardWidgets () { + return this.getWidgets('dashboard', 'defaultWidgets'); + } + + /** + * Creates an internal name for a dashboard based on its given name. + * + * @param {string} dashboardName - The name of the dashboard. + * @returns {string} - The internal name for the dashboard, formatted as `${dashboardName}Widgets`. + */ + createInternalDashboardName (dashboardName) { + return `${camelize(dashboardName.replace(/[^a-zA-Z0-9]/g, '-'))}Widgets`; + } + + /** + * Creates a new widget object from a widget definition. + * If the component is a function, it is registered with the host application. + * + * @param {Object} widget - The widget definition. + * @param {string} widget.widgetId - The unique ID of the widget. * @param {string} widget.name - The name of the widget. - * @param {string} widget.description - The description of the widget. - * @param {string} widget.icon - The icon for the widget. - * @param {(Function|string)} widget.component - The component class or name for the widget. - * @param {Object} widget.grid_options - Grid options for the widget layout. - * @param {Object} widget.options - Additional options for the widget. - * @returns {Object} A new widget object with properties derived from the input configuration. - * @memberof UniverseService + * @param {string} [widget.description] - A description of the widget. + * @param {string} [widget.icon] - An icon for the widget. + * @param {Function|string} [widget.component] - A component definition or name for the widget. + * @param {Object} [widget.grid_options] - Grid options for the widget. + * @param {Object} [widget.options] - Additional options for the widget. + * @returns {Object} - The newly created widget object. */ - _createDashboardWidget(widget) { + _createDashboardWidget (widget) { // Extract properties from the widget object let { widgetId, name, description, icon, component, grid_options, options } = widget; @@ -872,16 +1206,31 @@ export default class UniverseService extends Service.extend(Evented) { } /** - * Creates a unique hash from a component's definition. This hash is used as an identifier - * for the component when a direct identifier (widgetId) or a name is not available. + * Generates a unique hash for a widget component based on its function definition. + * This method delegates the hash creation to the `_createHashFromFunctionDefinition` method. * - * @param {Function} component - The component class or constructor function. - * @returns {string} A unique hash string representing the component's definition. - * @memberof UniverseService + * @param {Function} component - The function representing the widget component. + * @returns {string} - The unique hash representing the widget component. + */ + _createUniqueWidgetHashFromDefinition (component) { + return this._createHashFromFunctionDefinition(component); + } + + /** + * Creates a hash value from a function definition. The hash is generated based on the function's string representation. + * If the function has a name, it returns that name. Otherwise, it converts the function's string representation + * into a hash value. This is done by iterating over the characters of the string and performing a simple hash calculation. + * + * @param {Function} func - The function whose definition will be hashed. + * @returns {string} - The hash value derived from the function's definition. If the function has a name, it is returned directly. */ - _createUniqueWidgetHashFromDefinition(component) { - if (typeof component.toString === 'function') { - let definition = component.toString(); + _createHashFromFunctionDefinition (func) { + if (func.name) { + return func.name; + } + + if (typeof func.toString === 'function') { + let definition = func.toString(); let hash = 0; for (let i = 0; i < definition.length; i++) { const char = definition.charCodeAt(i); @@ -891,20 +1240,7 @@ export default class UniverseService extends Service.extend(Evented) { return hash.toString(16); } - return component.name; - } - - /** - * Registers a new settings menu item. - * - * @method registerSettingsMenuItem - * @public - * @memberof UniverseService - * @param {String} title The title of the item - * @param {Object} options Additional options for the item - */ - registerSettingsMenuItem(title, options = {}) { - this.registerMenuItem('settings', title, options); + return func.name; } /** @@ -917,7 +1253,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {String} route The route of the item * @param {Object} options Additional options for the item */ - registerHeaderMenuItem(title, route, options = {}) { + registerHeaderMenuItem (title, route, options = {}) { this.headerMenuItems.pushObject(this._createMenuItem(title, route, options)); this.headerMenuItems.sort((a, b) => a.priority - b.priority); } @@ -932,7 +1268,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {String} route The route of the item * @param {Object} options Additional options for the item */ - registerOrganizationMenuItem(title, options = {}) { + registerOrganizationMenuItem (title, options = {}) { const route = this._getOption(options, 'route', 'console.virtual'); options.index = this._getOption(options, 'index', 0); options.section = this._getOption(options, 'section', 'settings'); @@ -950,7 +1286,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {String} route The route of the item * @param {Object} options Additional options for the item */ - registerUserMenuItem(title, options = {}) { + registerUserMenuItem (title, options = {}) { const route = this._getOption(options, 'route', 'console.virtual'); options.index = this._getOption(options, 'index', 0); options.section = this._getOption(options, 'section', 'account'); @@ -969,7 +1305,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {*} defaultValue The default value if the key does not exist * @returns {*} The value of the key or default value */ - _getOption(target, key, defaultValue = null) { + _getOption (target, key, defaultValue = null) { return target[key] !== undefined ? target[key] : defaultValue; } @@ -984,7 +1320,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {Object} options Additional options for the item * @returns {Object} A new menu item object */ - _createMenuItem(title, route, options = {}) { + _createMenuItem (title, route, options = {}) { const priority = this._getOption(options, 'priority', 9); const icon = this._getOption(options, 'icon', 'circle-dot'); const items = this._getOption(options, 'items'); @@ -992,7 +1328,7 @@ export default class UniverseService extends Service.extend(Evented) { const componentParams = this._getOption(options, 'componentParams', {}); const renderComponentInPlace = this._getOption(options, 'renderComponentInPlace', false); const slug = this._getOption(options, 'slug', dasherize(title)); - const view = this._getOption(options, 'view'); + const view = this._getOption(options, 'view', dasherize(title)); const queryParams = this._getOption(options, 'queryParams', {}); const index = this._getOption(options, 'index', 0); const onClick = this._getOption(options, 'onClick', null); @@ -1007,12 +1343,17 @@ export default class UniverseService extends Service.extend(Evented) { const wrapperClass = this._getOption(options, 'wrapperClass', null); const overwriteWrapperClass = this._getOption(options, 'overwriteWrapperClass', false); const id = this._getOption(options, 'id', dasherize(title)); + const type = this._getOption(options, 'type', null); + const buttonType = this._getOption(options, 'buttonType', null); + const permission = this._getOption(options, 'permission', null); + const disabled = this._getOption(options, 'disabled', null); + const isLoading = this._getOption(options, 'isLoading', null); // dasherize route segments if (typeof route === 'string') { route = route .split('.') - .map((segment) => dasherize(segment)) + .map(segment => dasherize(segment)) .join('.'); } @@ -1043,6 +1384,11 @@ export default class UniverseService extends Service.extend(Evented) { inlineClass, wrapperClass, overwriteWrapperClass, + type, + buttonType, + permission, + disabled, + isLoading, }; return menuItem; @@ -1057,7 +1403,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {String} registryName - The name of the registry to be camelized and formatted. * @returns {String} The formatted internal registry name. */ - createInternalRegistryName(registryName) { + createInternalRegistryName (registryName) { return `${camelize(registryName.replace(/[^a-zA-Z0-9]/g, '-'))}Registry`; } @@ -1087,7 +1433,7 @@ export default class UniverseService extends Service.extend(Evented) { * - The custom name, if provided, must be a valid string. * - Allows flexible component referencing by registering under multiple names. */ - registerComponentInEngine(engineName, componentClass, options = {}) { + registerComponentInEngine (engineName, componentClass, options = {}) { const engineInstance = this.getEngineInstance(engineName); this.registerComponentToEngineInstance(engineInstance, componentClass, options); } @@ -1114,12 +1460,13 @@ export default class UniverseService extends Service.extend(Evented) { * - The custom name, if provided, is a valid string. * - This function directly manipulates the engine instance's registration map. */ - registerComponentToEngineInstance(engineInstance, componentClass, options = {}) { + registerComponentToEngineInstance (engineInstance, componentClass, options = {}) { if (engineInstance && componentClass && typeof componentClass.name === 'string') { engineInstance.register(`component:${componentClass.name}`, componentClass); engineInstance.register(`component:${dasherize(componentClass.name.replace('Component', ''))}`, componentClass); if (options && typeof options.registerAs === 'string') { engineInstance.register(`component:${options.registerAs}`, componentClass); + this.trigger('component.registered', componentClass, engineInstance); } } } @@ -1145,7 +1492,7 @@ export default class UniverseService extends Service.extend(Evented) { * - The service must exist in the current engine instance. * - The service is registered without instantiating a new copy in the target engine. */ - registerServiceInEngine(targetEngineName, serviceName, currentEngineInstance) { + registerServiceInEngine (targetEngineName, serviceName, currentEngineInstance) { // Get the target engine instance const targetEngineInstance = this.getEngineInstance(targetEngineName); @@ -1157,6 +1504,7 @@ export default class UniverseService extends Service.extend(Evented) { if (sharedService) { // Register the service in the target engine targetEngineInstance.register(`service:${serviceName}`, sharedService, { instantiate: false }); + this.trigger('service.registered', serviceName, targetEngineInstance); } } } @@ -1174,7 +1522,7 @@ export default class UniverseService extends Service.extend(Evented) { * userService.doSomething(); * } */ - getServiceFromEngine(engineName, serviceName, options = {}) { + getServiceFromEngine (engineName, serviceName, options = {}) { const engineInstance = this.getEngineInstance(engineName); if (engineInstance && typeof serviceName === 'string') { @@ -1200,7 +1548,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {String} name The name of the engine to load * @returns {Promise} A promise that resolves with the constructed engine instance */ - loadEngine(name) { + loadEngine (name) { const router = getOwner(this).lookup('router:main'); const instanceId = 'manual'; // Arbitrary instance id, should be unique per engine const mountPoint = this._mountPathFromEngineName(name); // No mount point for manually loaded engines @@ -1223,7 +1571,7 @@ export default class UniverseService extends Service.extend(Evented) { // The Engine is not loaded and has no Promise enginePromise = router._assetLoader.loadBundle(name).then( () => router._registerEngine(name), - (error) => { + error => { router._enginePromises[name][instanceId] = undefined; throw error; } @@ -1246,7 +1594,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {String} mountPoint The mount point of the engine * @returns {Promise} A promise that resolves with the constructed engine instance */ - constructEngineInstance(name, instanceId, mountPoint) { + constructEngineInstance (name, instanceId, mountPoint) { const owner = getOwner(this); assert("You attempted to load the engine '" + name + "', but the engine cannot be found.", owner.hasRegistration(`engine:${name}`)); @@ -1275,12 +1623,13 @@ export default class UniverseService extends Service.extend(Evented) { // store loaded instance to engineInstances for booting engineInstances[name][instanceId] = engineInstance; + this.trigger('engine.loaded', engineInstance); return engineInstance.boot().then(() => { return engineInstance; }); } - _setupEngineParentDependenciesBeforeBoot(baseDependencies = {}) { + _setupEngineParentDependenciesBeforeBoot (baseDependencies = {}) { const dependencies = { ...baseDependencies }; // fix services @@ -1328,7 +1677,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {String} [instanceId='manual'] The id of the engine instance (defaults to 'manual') * @returns {Object|null} The engine instance if it exists, otherwise null */ - getEngineInstance(name, instanceId = 'manual') { + getEngineInstance (name, instanceId = 'manual') { const owner = getOwner(this); const router = owner.lookup('router:main'); const engineInstances = router._engineInstances; @@ -1340,6 +1689,35 @@ export default class UniverseService extends Service.extend(Evented) { return null; } + /** + * Returns a promise that resolves when the `enginesBooted` property is set to true. + * The promise will reject with a timeout error if the property does not become true within the specified timeout. + * + * @function booting + * @returns {Promise} A promise that resolves when `enginesBooted` is true or rejects with an error after a timeout. + */ + booting () { + return new Promise((resolve, reject) => { + const check = () => { + if (this.enginesBooted === true) { + this.trigger('booted'); + clearInterval(intervalId); + resolve(); + } + }; + + const intervalId = setInterval(check, 100); + later( + this, + () => { + clearInterval(intervalId); + reject(new Error('Timeout: Universe was unable to boot engines')); + }, + 5000 + ); + }); + } + /** * Boot all installed engines, ensuring dependencies are resolved. * @@ -1352,7 +1730,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {ApplicationInstance|null} owner - The Ember ApplicationInstance that owns the engines. * @return {void} */ - bootEngines(owner = null) { + bootEngines (owner = null) { const booted = []; const pending = []; const additionalCoreExtensions = config.APP.extensions ?? []; @@ -1362,15 +1740,18 @@ export default class UniverseService extends Service.extend(Evented) { owner = getOwner(this); } - const tryBootEngine = (extension) => { - this.loadEngine(extension.name).then((engineInstance) => { + // Set application instance + this.applicationInstance = owner; + + const tryBootEngine = extension => { + this.loadEngine(extension.name).then(engineInstance => { if (engineInstance.base && engineInstance.base.setupExtension) { - if (booted.includes(extension.name)) { + if (this.bootedExtensions.includes(extension.name)) { return; } const engineDependencies = getWithDefault(engineInstance.base, 'engineDependencies', []); - const allDependenciesBooted = engineDependencies.every((dep) => booted.includes(dep)); + const allDependenciesBooted = engineDependencies.every(dep => booted.includes(dep)); if (!allDependenciesBooted) { pending.push({ extension, engineInstance }); @@ -1379,6 +1760,8 @@ export default class UniverseService extends Service.extend(Evented) { engineInstance.base.setupExtension(owner, engineInstance, this); booted.push(extension.name); + this.bootedExtensions.pushObject(extension.name); + this.trigger('extension.booted', extension); debug(`Booted : ${extension.name}`); // Try booting pending engines again @@ -1391,16 +1774,18 @@ export default class UniverseService extends Service.extend(Evented) { const stillPending = []; pending.forEach(({ extension, engineInstance }) => { - if (booted.includes(extension.name)) { + if (this.bootedExtensions.includes(extension.name)) { return; } const engineDependencies = getWithDefault(engineInstance.base, 'engineDependencies', []); - const allDependenciesBooted = engineDependencies.every((dep) => booted.includes(dep)); + const allDependenciesBooted = engineDependencies.every(dep => booted.includes(dep)); if (allDependenciesBooted) { engineInstance.base.setupExtension(owner, engineInstance, this); booted.push(extension.name); + this.bootedExtensions.pushObject(extension.name); + this.trigger('extension.booted', extension); debug(`Booted : ${extension.name}`); } else { stillPending.push({ extension, engineInstance }); @@ -1414,10 +1799,12 @@ export default class UniverseService extends Service.extend(Evented) { pending.push(...stillPending); }; - loadInstalledExtensions(additionalCoreExtensions).then((extensions) => { - extensions.forEach((extension) => { + return loadInstalledExtensions(additionalCoreExtensions).then(extensions => { + extensions.forEach(extension => { tryBootEngine(extension); }); + + this.enginesBooted = true; }); } @@ -1434,7 +1821,7 @@ export default class UniverseService extends Service.extend(Evented) { * @param {ApplicationInstance|null} owner - The Ember ApplicationInstance that owns the engines. * @return {void} */ - legacyBootEngines(owner = null) { + legacyBootEngines (owner = null) { const booted = []; const pending = []; @@ -1443,13 +1830,16 @@ export default class UniverseService extends Service.extend(Evented) { owner = getOwner(this); } - const tryBootEngine = (extension) => { - this.loadEngine(extension.name).then((engineInstance) => { + // Set application instance + this.applicationInstance = owner; + + const tryBootEngine = extension => { + this.loadEngine(extension.name).then(engineInstance => { if (engineInstance.base && engineInstance.base.setupExtension) { const engineDependencies = getWithDefault(engineInstance.base, 'engineDependencies', []); // Check if all dependency engines are booted - const allDependenciesBooted = engineDependencies.every((dep) => booted.includes(dep)); + const allDependenciesBooted = engineDependencies.every(dep => booted.includes(dep)); if (!allDependenciesBooted) { pending.push({ extension, engineInstance }); @@ -1458,6 +1848,8 @@ export default class UniverseService extends Service.extend(Evented) { engineInstance.base.setupExtension(owner, engineInstance, this); booted.push(extension.name); + this.bootedExtensions.pushObject(extension.name); + this.trigger('extension.booted', extension); debug(`Booted : ${extension.name}`); // Try booting pending engines again @@ -1471,11 +1863,13 @@ export default class UniverseService extends Service.extend(Evented) { pending.forEach(({ extension, engineInstance }) => { const engineDependencies = getWithDefault(engineInstance.base, 'engineDependencies', []); - const allDependenciesBooted = engineDependencies.every((dep) => booted.includes(dep)); + const allDependenciesBooted = engineDependencies.every(dep => booted.includes(dep)); if (allDependenciesBooted) { engineInstance.base.setupExtension(owner, engineInstance, this); booted.push(extension.name); + this.bootedExtensions.pushObject(extension.name); + this.trigger('extension.booted', extension); debug(`Booted : ${extension.name}`); } else { stillPending.push({ extension, engineInstance }); @@ -1489,19 +1883,32 @@ export default class UniverseService extends Service.extend(Evented) { pending.push(...stillPending); }; - loadExtensions().then((extensions) => { - extensions.forEach((extension) => { + return loadExtensions().then(extensions => { + extensions.forEach(extension => { tryBootEngine(extension); }); + + this.enginesBooted = true; }); } + /** + * Checks if an extension has been booted. + * + * @param {String} name + * @return {Boolean} + * @memberof UniverseService + */ + didBootEngine (name) { + return this.bootedExtensions.includes(name); + } + /** * Alias for intl service `t` * * @memberof UniverseService */ - t() { + t () { this.intl.t(...arguments); } } diff --git a/addon/utils/is-empty-object.js b/addon/utils/is-empty-object.js index 28073d2..374dcd4 100644 --- a/addon/utils/is-empty-object.js +++ b/addon/utils/is-empty-object.js @@ -1,3 +1,9 @@ -export default function isEmptyObject(obj) { - return Object.keys(obj).length === 0 && obj.constructor === Object; +import { isBlank } from '@ember/utils'; + +export default function isEmptyObject (obj) { + if (isBlank(obj)) { + return true; + } + + return obj.constructor === Object && Object.keys(obj).length === 0; } diff --git a/addon/utils/serialize/normalize-relations-with-hash.js b/addon/utils/serialize/normalize-relations-with-hash.js index bf1b9a5..4a335ca 100644 --- a/addon/utils/serialize/normalize-relations-with-hash.js +++ b/addon/utils/serialize/normalize-relations-with-hash.js @@ -1,24 +1,19 @@ import { get, setProperties } from '@ember/object'; import { isBlank } from '@ember/utils'; -export default function serializeNormalizeRelationsWithinHash(hash, primaryKey = 'uuid') { +export default function serializeNormalizeRelationsWithinHash (hash, primaryKey = 'uuid') { if (typeof hash !== 'object') { return hash; } for (let attr in hash) { if (typeof attr === 'string' && attr.includes('_uuid')) { - // console.log(attr, hash[attr]); if (typeof hash[attr] === 'object' && !isBlank(hash[attr])) { // the relation has loaded back into `_uuid` - change this to the proper `uuid` string value and set the relationship const relation = hash[attr]; const id = get(relation, primaryKey); const relationAttr = attr.replace('_uuid', ''); - // console.log(`normalizing relation ${relationAttr} from ${attr}`); - // console.log(relationAttr, relation); - // console.log(attr, id); - setProperties(hash, { [relationAttr]: relation, [attr]: id, diff --git a/package.json b/package.json index 1f3d84a..7553690 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fleetbase/ember-core", - "version": "0.2.17", + "version": "0.2.18", "description": "Provides all the core services, decorators and utilities for building a Fleetbase extension for the Console.", "keywords": [ "fleetbase-core",