From 2146a838da7f82434e7a29525abf8740391df417 Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson" <me@ron.dev>
Date: Wed, 25 Sep 2024 14:01:14 +0800
Subject: [PATCH 1/5] new container register utilites and optimization on
 current user service

---
 addon/services/current-user.js              | 28 +++++++++++++++++----
 addon/services/fetch.js                     |  1 +
 addon/utils/register-component.js           |  8 ++++++
 addon/utils/register-helper.js              |  8 ++++++
 app/utils/register-component.js             |  1 +
 app/utils/register-helper.js                |  1 +
 package.json                                |  2 +-
 tests/unit/utils/register-component-test.js | 10 ++++++++
 tests/unit/utils/register-helper-test.js    | 10 ++++++++
 9 files changed, 63 insertions(+), 6 deletions(-)
 create mode 100644 addon/utils/register-component.js
 create mode 100644 addon/utils/register-helper.js
 create mode 100644 app/utils/register-component.js
 create mode 100644 app/utils/register-helper.js
 create mode 100644 tests/unit/utils/register-component-test.js
 create mode 100644 tests/unit/utils/register-helper-test.js

diff --git a/addon/services/current-user.js b/addon/services/current-user.js
index 726b603..804c02b 100644
--- a/addon/services/current-user.js
+++ b/addon/services/current-user.js
@@ -88,14 +88,26 @@ export default class CurrentUserService extends Service.extend(Evented) {
             this.set('user', user);
             this.trigger('user.loaded', user);
 
+            console.log(user);
+
             // Set permissions
             this.permissions = this.getUserPermissions(user);
 
             // Set environment from user option
             this.theme.setEnvironment();
 
-            // Load user preferces
-            await this.loadPreferences();
+            // Set locale
+            if (user.locale) {
+                this.setLocale(user.locale);
+            } else {
+                await this.loadLocale();
+            }
+
+            // Load user whois data
+            await this.loadWhois();
+
+            // Load user organizations
+            await this.loadOrganizations();
 
             // Optional callback
             if (typeof options?.onUserResolved === 'function') {
@@ -118,9 +130,7 @@ export default class CurrentUserService extends Service.extend(Evented) {
     async loadLocale() {
         try {
             const { locale } = await this.fetch.get('users/locale');
-            this.setOption('locale', locale);
-            this.intl.setLocale(locale);
-            this.locale = locale;
+            this.setLocale(locale);
 
             return locale;
         } catch (error) {
@@ -207,6 +217,14 @@ export default class CurrentUserService extends Service.extend(Evented) {
         return this.getWhoisProperty(key);
     }
 
+    setLocale(locale) {
+        this.setOption('locale', locale);
+        this.intl.setLocale(locale);
+        this.locale = locale;
+
+        return this;
+    }
+
     setOption(key, value) {
         key = `${this.optionsPrefix}${dasherize(key)}`;
 
diff --git a/addon/services/fetch.js b/addon/services/fetch.js
index 79fbcb6..0e0ee84 100644
--- a/addon/services/fetch.js
+++ b/addon/services/fetch.js
@@ -360,6 +360,7 @@ export default class FetchService extends Service {
         const pathKeyVersion = new Date().toISOString();
 
         const request = () => {
+            delete options.fromCache;
             return this.get(path, query, options).then((response) => {
                 // cache the response
                 this.localCache.set(pathKey, response);
diff --git a/addon/utils/register-component.js b/addon/utils/register-component.js
new file mode 100644
index 0000000..1c6df1c
--- /dev/null
+++ b/addon/utils/register-component.js
@@ -0,0 +1,8 @@
+import { dasherize } from '@ember/string';
+
+export default function registerComponent(owner, componentClass, options = {}) {
+    const registrationName = options && options.as ? `component:${options.as}` : `component:${dasherize(componentClass.name).replace('-component', '')}`;
+    if (!owner.hasRegistration(registrationName)) {
+        owner.register(registrationName, componentClass);
+    }
+}
diff --git a/addon/utils/register-helper.js b/addon/utils/register-helper.js
new file mode 100644
index 0000000..396a64f
--- /dev/null
+++ b/addon/utils/register-helper.js
@@ -0,0 +1,8 @@
+import { dasherize } from '@ember/string';
+
+export default function registerHelper(owner, name, helperFn) {
+    const registrationName = `helper:${dasherize(name)}`;
+    if (!owner.hasRegistration(registrationName)) {
+        owner.register(registrationName, helperFn);
+    }
+}
diff --git a/app/utils/register-component.js b/app/utils/register-component.js
new file mode 100644
index 0000000..af6623f
--- /dev/null
+++ b/app/utils/register-component.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/ember-core/utils/register-component';
diff --git a/app/utils/register-helper.js b/app/utils/register-helper.js
new file mode 100644
index 0000000..2f651db
--- /dev/null
+++ b/app/utils/register-helper.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/ember-core/utils/register-helper';
diff --git a/package.json b/package.json
index 7553690..923dbed 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "@fleetbase/ember-core",
-    "version": "0.2.18",
+    "version": "0.2.19",
     "description": "Provides all the core services, decorators and utilities for building a Fleetbase extension for the Console.",
     "keywords": [
         "fleetbase-core",
diff --git a/tests/unit/utils/register-component-test.js b/tests/unit/utils/register-component-test.js
new file mode 100644
index 0000000..ec3f9f5
--- /dev/null
+++ b/tests/unit/utils/register-component-test.js
@@ -0,0 +1,10 @@
+import registerComponent from 'dummy/utils/register-component';
+import { module, test } from 'qunit';
+
+module('Unit | Utility | register-component', function () {
+    // TODO: Replace this with your real tests.
+    test('it works', function (assert) {
+        let result = registerComponent();
+        assert.ok(result);
+    });
+});
diff --git a/tests/unit/utils/register-helper-test.js b/tests/unit/utils/register-helper-test.js
new file mode 100644
index 0000000..57b49c1
--- /dev/null
+++ b/tests/unit/utils/register-helper-test.js
@@ -0,0 +1,10 @@
+import registerHelper from 'dummy/utils/register-helper';
+import { module, test } from 'qunit';
+
+module('Unit | Utility | register-helper', function () {
+    // TODO: Replace this with your real tests.
+    test('it works', function (assert) {
+        let result = registerHelper();
+        assert.ok(result);
+    });
+});

From 55b1a19bf17e2b1f1b7ab40a1aa53f1224257771 Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson" <me@ron.dev>
Date: Wed, 25 Sep 2024 14:55:56 +0800
Subject: [PATCH 2/5] ran linter

---
 addon/services/current-user.js | 44 +++++++++--------
 addon/services/fetch.js        | 86 +++++++++++++++++++---------------
 addon/services/universe.js     |  4 ++
 3 files changed, 73 insertions(+), 61 deletions(-)

diff --git a/addon/services/current-user.js b/addon/services/current-user.js
index 804c02b..b0b3d58 100644
--- a/addon/services/current-user.js
+++ b/addon/services/current-user.js
@@ -33,31 +33,31 @@ export default class CurrentUserService extends Service.extend(Evented) {
     @alias('user.company_uuid') companyId;
     @alias('user.company_name') companyName;
 
-    @computed('id') get optionsPrefix() {
+    @computed('id') get optionsPrefix () {
         return `${this.id}:`;
     }
 
-    get latitude() {
+    get latitude () {
         return this.whois('latitude');
     }
 
-    get longitude() {
+    get longitude () {
         return this.whois('longitude');
     }
 
-    get currency() {
+    get currency () {
         return this.whois('currency.code');
     }
 
-    get city() {
+    get city () {
         return this.whois('city');
     }
 
-    get country() {
+    get country () {
         return this.whois('country_code');
     }
 
-    async load() {
+    async load () {
         if (this.session.isAuthenticated) {
             const user = await this.store.findRecord('user', 'me');
             this.set('user', user);
@@ -75,7 +75,7 @@ export default class CurrentUserService extends Service.extend(Evented) {
         return null;
     }
 
-    async promiseUser(options = {}) {
+    async promiseUser (options = {}) {
         const NoUserAuthenticatedError = new Error('Failed to authenticate user.');
         if (!this.session.isAuthenticated) {
             throw NoUserAuthenticatedError;
@@ -88,8 +88,6 @@ export default class CurrentUserService extends Service.extend(Evented) {
             this.set('user', user);
             this.trigger('user.loaded', user);
 
-            console.log(user);
-
             // Set permissions
             this.permissions = this.getUserPermissions(user);
 
@@ -121,13 +119,13 @@ export default class CurrentUserService extends Service.extend(Evented) {
         }
     }
 
-    async loadPreferences() {
+    async loadPreferences () {
         await this.loadLocale();
         await this.loadWhois();
         await this.loadOrganizations();
     }
 
-    async loadLocale() {
+    async loadLocale () {
         try {
             const { locale } = await this.fetch.get('users/locale');
             this.setLocale(locale);
@@ -138,7 +136,7 @@ export default class CurrentUserService extends Service.extend(Evented) {
         }
     }
 
-    async loadOrganizations() {
+    async loadOrganizations () {
         try {
             const organizations = await this.fetch.get('auth/organizations', {}, { normalizeToEmberData: true, normalizeModelType: 'company' });
             this.setOption('organizations', organizations);
@@ -150,7 +148,7 @@ export default class CurrentUserService extends Service.extend(Evented) {
         }
     }
 
-    async loadWhois() {
+    async loadWhois () {
         this.fetch.shouldResetCache();
 
         try {
@@ -171,12 +169,12 @@ export default class CurrentUserService extends Service.extend(Evented) {
         }
     }
 
-    getCompany() {
+    getCompany () {
         this.company = this.store.peekRecord('company', this.user.company_uuid);
         return this.company;
     }
 
-    getUserPermissions(user) {
+    getUserPermissions (user) {
         const permissions = [];
 
         // get direct applied permissions
@@ -213,11 +211,11 @@ export default class CurrentUserService extends Service.extend(Evented) {
         return permissions;
     }
 
-    whois(key) {
+    whois (key) {
         return this.getWhoisProperty(key);
     }
 
-    setLocale(locale) {
+    setLocale (locale) {
         this.setOption('locale', locale);
         this.intl.setLocale(locale);
         this.locale = locale;
@@ -225,7 +223,7 @@ export default class CurrentUserService extends Service.extend(Evented) {
         return this;
     }
 
-    setOption(key, value) {
+    setOption (key, value) {
         key = `${this.optionsPrefix}${dasherize(key)}`;
 
         this.options.set(key, value);
@@ -233,14 +231,14 @@ export default class CurrentUserService extends Service.extend(Evented) {
         return this;
     }
 
-    getOption(key, defaultValue = null) {
+    getOption (key, defaultValue = null) {
         key = `${this.optionsPrefix}${dasherize(key)}`;
 
         const value = this.options.get(key);
         return value !== undefined ? value : defaultValue;
     }
 
-    getWhoisProperty(prop) {
+    getWhoisProperty (prop) {
         const whois = this.getOption('whois');
 
         if (!whois || typeof whois !== 'object') {
@@ -250,11 +248,11 @@ export default class CurrentUserService extends Service.extend(Evented) {
         return get(whois, prop);
     }
 
-    hasOption(key) {
+    hasOption (key) {
         return this.getOption(key) !== undefined;
     }
 
-    filledOption(key) {
+    filledOption (key) {
         return !isBlank(this.getOption(key));
     }
 }
diff --git a/addon/services/fetch.js b/addon/services/fetch.js
index 0e0ee84..572176b 100644
--- a/addon/services/fetch.js
+++ b/addon/services/fetch.js
@@ -27,7 +27,7 @@ export default class FetchService extends Service {
      * Creates an instance of FetchService.
      * @memberof FetchService
      */
-    constructor() {
+    constructor () {
         super(...arguments);
 
         this.headers = this.getHeaders();
@@ -61,7 +61,7 @@ export default class FetchService extends Service {
      *
      * @return {Object}
      */
-    getHeaders() {
+    getHeaders () {
         const headers = {};
         const isAuthenticated = this.session.isAuthenticated;
         const userId = this.session.data.authenticated.user;
@@ -92,7 +92,7 @@ export default class FetchService extends Service {
      * @return {FetchService}
      * @memberof FetchService
      */
-    refreshHeaders() {
+    refreshHeaders () {
         this.headers = this.getHeaders();
 
         return this;
@@ -105,7 +105,7 @@ export default class FetchService extends Service {
      * @return {FetchService}
      * @memberof FetchService
      */
-    setNamespace(namespace) {
+    setNamespace (namespace) {
         this.namespace = namespace;
 
         return this;
@@ -118,7 +118,7 @@ export default class FetchService extends Service {
      * @return {FetchService}
      * @memberof FetchService
      */
-    setHost(host) {
+    setHost (host) {
         this.host = host;
 
         return this;
@@ -174,7 +174,7 @@ export default class FetchService extends Service {
      *
      * @return {Model}            An ember model
      */
-    normalizeModel(payload, modelType = null) {
+    normalizeModel (payload, modelType = null) {
         if (modelType === null) {
             const modelTypeKeys = Object.keys(payload);
             modelType = modelTypeKeys.length ? modelTypeKeys.firstObject : false;
@@ -187,11 +187,11 @@ export default class FetchService extends Service {
         const type = dasherize(singularize(modelType));
 
         if (isArray(payload)) {
-            return payload.map((instance) => this.store.push(this.store.normalize(type, instance)));
+            return payload.map(instance => this.store.push(this.store.normalize(type, instance)));
         }
 
         if (isArray(payload[modelType])) {
-            return payload[modelType].map((instance) => this.store.push(this.store.normalize(type, instance)));
+            return payload[modelType].map(instance => this.store.push(this.store.normalize(type, instance)));
         }
 
         if (!isBlank(payload) && isBlank(payload[modelType])) {
@@ -209,7 +209,7 @@ export default class FetchService extends Service {
      *
      * @return {Model}            An ember model
      */
-    jsonToModel(attributes = {}, modelType) {
+    jsonToModel (attributes = {}, modelType) {
         if (typeof attributes === 'string') {
             attributes = JSON.parse(attributes);
         }
@@ -228,7 +228,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    async parseJSON(response) {
+    async parseJSON (response) {
         try {
             const compressedHeader = await response.headers.get('x-compressed-json');
             let json;
@@ -265,7 +265,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    request(path, method = 'GET', data = {}, options = {}) {
+    request (path, method = 'GET', data = {}, options = {}) {
         const headers = Object.assign(this.getHeaders(), options.headers ?? {});
         const host = options.host ?? this.host;
         const namespace = options.namespace ?? this.namespace;
@@ -280,7 +280,7 @@ export default class FetchService extends Service {
                 ...data,
             })
                 .then(this.parseJSON)
-                .then((response) => {
+                .then(response => {
                     if (response.ok) {
                         if (options.normalizeToEmberData) {
                             const normalized = this.normalizeModel(response.json, options.normalizeModelType);
@@ -334,7 +334,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    get(path, query = {}, options = {}) {
+    get (path, query = {}, options = {}) {
         // handle if want to request from cache
         if (options.fromCache === true) {
             return this.cachedGet(...arguments);
@@ -355,13 +355,13 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    cachedGet(path, query = {}, options = {}) {
+    cachedGet (path, query = {}, options = {}) {
         const pathKey = dasherize(path);
         const pathKeyVersion = new Date().toISOString();
 
         const request = () => {
             delete options.fromCache;
-            return this.get(path, query, options).then((response) => {
+            return this.get(path, query, options).then(response => {
                 // cache the response
                 this.localCache.set(pathKey, response);
                 this.localCache.set(`${pathKey}-version`, pathKeyVersion);
@@ -373,7 +373,7 @@ export default class FetchService extends Service {
 
         // check to see if in storage already
         if (this.localCache.get(pathKey)) {
-            return new Promise((resolve) => {
+            return new Promise(resolve => {
                 // get cached data
                 const data = this.localCache.get(pathKey);
 
@@ -409,14 +409,24 @@ export default class FetchService extends Service {
         return request();
     }
 
-    flushRequestCache(path) {
+    /**
+     * Flushes the local cache for a specific path by setting its value and version to undefined.
+     *
+     * @param {string} path - The path for which the cache should be flushed.
+     */
+    flushRequestCache (path) {
         const pathKey = dasherize(path);
 
         this.localCache.set(pathKey, undefined);
         this.localCache.set(`${pathKey}-version`, undefined);
     }
 
-    shouldResetCache() {
+    /**
+     * Determines whether the cache should be reset by comparing the current version
+     * of the console with the cached version. If they differ, the cache is cleared
+     * and the new version is saved.
+     */
+    shouldResetCache () {
         const consoleVersion = this.localCache.get('console-version');
 
         if (!consoleVersion || consoleVersion !== config.APP.version) {
@@ -434,7 +444,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    post(path, data = {}, options = {}) {
+    post (path, data = {}, options = {}) {
         return this.request(path, 'POST', { body: JSON.stringify(data) }, options);
     }
 
@@ -447,7 +457,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    put(path, data = {}, options = {}) {
+    put (path, data = {}, options = {}) {
         return this.request(path, 'PUT', { body: JSON.stringify(data) }, options);
     }
 
@@ -460,7 +470,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    delete(path, data = {}, options = {}) {
+    delete (path, data = {}, options = {}) {
         return this.request(path, 'DELETE', { body: JSON.stringify(data) }, options);
     }
 
@@ -472,7 +482,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    patch(path, data = {}, options = {}) {
+    patch (path, data = {}, options = {}) {
         return this.request(path, 'PATCH', { body: JSON.stringify(data) }, options);
     }
 
@@ -485,9 +495,9 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    upload(path, files = [], options = {}) {
+    upload (path, files = [], options = {}) {
         const body = new FormData();
-        files.forEach((file) => {
+        files.forEach(file => {
             body.append('file', file);
         });
         return this.request(path, 'POST', { body }, options);
@@ -502,12 +512,12 @@ export default class FetchService extends Service {
      * @param {String} profile
      * @param {String} version
      */
-    routing(coordinates, query = {}, options = {}) {
+    routing (coordinates, query = {}, options = {}) {
         let service = options?.service ?? 'trip';
         let profile = options?.profile ?? 'driving';
         let version = options?.version ?? 'v1';
         let host = options?.host ?? `https://${options?.subdomain ?? 'routing'}.fleetbase.io`;
-        let route = coordinates.map((coords) => coords.join(',')).join(';');
+        let route = coordinates.map(coords => coords.join(',')).join(';');
         let params = !isEmptyObject(query) ? new URLSearchParams(query).toString() : '';
         let path = `${host}/${service}/${version}/${profile}/${route}`;
         let url = `${path}${params ? '?' + params : ''}`;
@@ -555,8 +565,8 @@ export default class FetchService extends Service {
                     withCredentials: true,
                     headers,
                 })
-                .then((response) => response.json())
-                .catch((error) => {
+                .then(response => response.json())
+                .catch(error => {
                     this.notifications.serverError(error, 'File upload failed.');
 
                     if (typeof errorCallback === 'function') {
@@ -598,7 +608,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    download(path, query = {}, options = {}) {
+    download (path, query = {}, options = {}) {
         const headers = Object.assign(this.getHeaders(), options.headers ?? {});
         const method = options.method ?? 'GET';
         const credentials = options.credentials ?? this.credentials;
@@ -619,7 +629,7 @@ export default class FetchService extends Service {
 
         return new Promise((resolve, reject) => {
             return fetch(`${baseUrl}/${path}${params}`, fetchOptions)
-                .then((response) => {
+                .then(response => {
                     options.fileName = this.getFilenameFromResponse(response, options.fileName);
                     options.mimeType = this.getMimeTypeFromResponse(response, options.mimeType);
 
@@ -629,15 +639,15 @@ export default class FetchService extends Service {
 
                     return response;
                 })
-                .then((response) => response.blob())
-                .then((blob) => resolve(download(blob, options.fileName, options.mimeType)))
-                .catch((error) => {
+                .then(response => response.blob())
+                .then(blob => resolve(download(blob, options.fileName, options.mimeType)))
+                .catch(error => {
                     reject(error);
                 });
         });
     }
 
-    getFilenameFromResponse(response, defaultFilename = null) {
+    getFilenameFromResponse (response, defaultFilename = null) {
         const contentDisposition = response.headers.get('content-disposition');
         let fileName = defaultFilename;
 
@@ -655,7 +665,7 @@ export default class FetchService extends Service {
         return fileName;
     }
 
-    getMimeTypeFromResponse(response, defaultMimeType = null) {
+    getMimeTypeFromResponse (response, defaultMimeType = null) {
         const contentType = response.headers.get('content-type');
         let mimeType = defaultMimeType;
 
@@ -670,10 +680,10 @@ export default class FetchService extends Service {
         return mimeType;
     }
 
-    fetchOrderConfigurations(params = {}) {
+    fetchOrderConfigurations (params = {}) {
         return new Promise((resolve, reject) => {
             this.request('fleet-ops/order-configs/get-installed', params)
-                .then((configs) => {
+                .then(configs => {
                     const serialized = [];
 
                     for (let i = 0; i < configs.length; i++) {
@@ -686,7 +696,7 @@ export default class FetchService extends Service {
 
                     resolve(serialized);
                 })
-                .catch((error) => {
+                .catch(error => {
                     reject(error);
                 });
         });
diff --git a/addon/services/universe.js b/addon/services/universe.js
index ca3cb8f..e9f71a2 100644
--- a/addon/services/universe.js
+++ b/addon/services/universe.js
@@ -777,9 +777,11 @@ export default class UniverseService extends Service.extend(Evented) {
      */
     registerMenuPanel(registryName, title, items = [], options = {}) {
         const internalRegistryName = this.createInternalRegistryName(registryName);
+        const intl = this._getOption(options, 'intl', null);
         const open = this._getOption(options, 'open', true);
         const slug = this._getOption(options, 'slug', dasherize(title));
         const menuPanel = {
+            intl,
             title,
             open,
             items: items.map(({ title, route, ...options }) => {
@@ -1321,6 +1323,7 @@ export default class UniverseService extends Service.extend(Evented) {
      * @returns {Object} A new menu item object
      */
     _createMenuItem(title, route, options = {}) {
+        const intl = this._getOption(options, 'intl', null);
         const priority = this._getOption(options, 'priority', 9);
         const icon = this._getOption(options, 'icon', 'circle-dot');
         const items = this._getOption(options, 'items');
@@ -1360,6 +1363,7 @@ export default class UniverseService extends Service.extend(Evented) {
         // @todo: create menu item class
         const menuItem = {
             id,
+            intl,
             title,
             text: title,
             route,

From 4c94235e9aeb2de0b9ede5df9a35a836c07e89b0 Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson" <me@ron.dev>
Date: Wed, 25 Sep 2024 15:49:28 +0800
Subject: [PATCH 3/5] fixed cachedGet on fetch method using resolve in cache
 promise

---
 addon/services/current-user.js | 42 +++++++++---------
 addon/services/fetch.js        | 78 +++++++++++++++++-----------------
 2 files changed, 60 insertions(+), 60 deletions(-)

diff --git a/addon/services/current-user.js b/addon/services/current-user.js
index b0b3d58..0a36343 100644
--- a/addon/services/current-user.js
+++ b/addon/services/current-user.js
@@ -33,31 +33,31 @@ export default class CurrentUserService extends Service.extend(Evented) {
     @alias('user.company_uuid') companyId;
     @alias('user.company_name') companyName;
 
-    @computed('id') get optionsPrefix () {
+    @computed('id') get optionsPrefix() {
         return `${this.id}:`;
     }
 
-    get latitude () {
+    get latitude() {
         return this.whois('latitude');
     }
 
-    get longitude () {
+    get longitude() {
         return this.whois('longitude');
     }
 
-    get currency () {
+    get currency() {
         return this.whois('currency.code');
     }
 
-    get city () {
+    get city() {
         return this.whois('city');
     }
 
-    get country () {
+    get country() {
         return this.whois('country_code');
     }
 
-    async load () {
+    async load() {
         if (this.session.isAuthenticated) {
             const user = await this.store.findRecord('user', 'me');
             this.set('user', user);
@@ -75,7 +75,7 @@ export default class CurrentUserService extends Service.extend(Evented) {
         return null;
     }
 
-    async promiseUser (options = {}) {
+    async promiseUser(options = {}) {
         const NoUserAuthenticatedError = new Error('Failed to authenticate user.');
         if (!this.session.isAuthenticated) {
             throw NoUserAuthenticatedError;
@@ -119,13 +119,13 @@ export default class CurrentUserService extends Service.extend(Evented) {
         }
     }
 
-    async loadPreferences () {
+    async loadPreferences() {
         await this.loadLocale();
         await this.loadWhois();
         await this.loadOrganizations();
     }
 
-    async loadLocale () {
+    async loadLocale() {
         try {
             const { locale } = await this.fetch.get('users/locale');
             this.setLocale(locale);
@@ -136,7 +136,7 @@ export default class CurrentUserService extends Service.extend(Evented) {
         }
     }
 
-    async loadOrganizations () {
+    async loadOrganizations() {
         try {
             const organizations = await this.fetch.get('auth/organizations', {}, { normalizeToEmberData: true, normalizeModelType: 'company' });
             this.setOption('organizations', organizations);
@@ -148,7 +148,7 @@ export default class CurrentUserService extends Service.extend(Evented) {
         }
     }
 
-    async loadWhois () {
+    async loadWhois() {
         this.fetch.shouldResetCache();
 
         try {
@@ -169,12 +169,12 @@ export default class CurrentUserService extends Service.extend(Evented) {
         }
     }
 
-    getCompany () {
+    getCompany() {
         this.company = this.store.peekRecord('company', this.user.company_uuid);
         return this.company;
     }
 
-    getUserPermissions (user) {
+    getUserPermissions(user) {
         const permissions = [];
 
         // get direct applied permissions
@@ -211,11 +211,11 @@ export default class CurrentUserService extends Service.extend(Evented) {
         return permissions;
     }
 
-    whois (key) {
+    whois(key) {
         return this.getWhoisProperty(key);
     }
 
-    setLocale (locale) {
+    setLocale(locale) {
         this.setOption('locale', locale);
         this.intl.setLocale(locale);
         this.locale = locale;
@@ -223,7 +223,7 @@ export default class CurrentUserService extends Service.extend(Evented) {
         return this;
     }
 
-    setOption (key, value) {
+    setOption(key, value) {
         key = `${this.optionsPrefix}${dasherize(key)}`;
 
         this.options.set(key, value);
@@ -231,14 +231,14 @@ export default class CurrentUserService extends Service.extend(Evented) {
         return this;
     }
 
-    getOption (key, defaultValue = null) {
+    getOption(key, defaultValue = null) {
         key = `${this.optionsPrefix}${dasherize(key)}`;
 
         const value = this.options.get(key);
         return value !== undefined ? value : defaultValue;
     }
 
-    getWhoisProperty (prop) {
+    getWhoisProperty(prop) {
         const whois = this.getOption('whois');
 
         if (!whois || typeof whois !== 'object') {
@@ -248,11 +248,11 @@ export default class CurrentUserService extends Service.extend(Evented) {
         return get(whois, prop);
     }
 
-    hasOption (key) {
+    hasOption(key) {
         return this.getOption(key) !== undefined;
     }
 
-    filledOption (key) {
+    filledOption(key) {
         return !isBlank(this.getOption(key));
     }
 }
diff --git a/addon/services/fetch.js b/addon/services/fetch.js
index 572176b..2024343 100644
--- a/addon/services/fetch.js
+++ b/addon/services/fetch.js
@@ -27,7 +27,7 @@ export default class FetchService extends Service {
      * Creates an instance of FetchService.
      * @memberof FetchService
      */
-    constructor () {
+    constructor() {
         super(...arguments);
 
         this.headers = this.getHeaders();
@@ -61,7 +61,7 @@ export default class FetchService extends Service {
      *
      * @return {Object}
      */
-    getHeaders () {
+    getHeaders() {
         const headers = {};
         const isAuthenticated = this.session.isAuthenticated;
         const userId = this.session.data.authenticated.user;
@@ -92,7 +92,7 @@ export default class FetchService extends Service {
      * @return {FetchService}
      * @memberof FetchService
      */
-    refreshHeaders () {
+    refreshHeaders() {
         this.headers = this.getHeaders();
 
         return this;
@@ -105,7 +105,7 @@ export default class FetchService extends Service {
      * @return {FetchService}
      * @memberof FetchService
      */
-    setNamespace (namespace) {
+    setNamespace(namespace) {
         this.namespace = namespace;
 
         return this;
@@ -118,7 +118,7 @@ export default class FetchService extends Service {
      * @return {FetchService}
      * @memberof FetchService
      */
-    setHost (host) {
+    setHost(host) {
         this.host = host;
 
         return this;
@@ -174,7 +174,7 @@ export default class FetchService extends Service {
      *
      * @return {Model}            An ember model
      */
-    normalizeModel (payload, modelType = null) {
+    normalizeModel(payload, modelType = null) {
         if (modelType === null) {
             const modelTypeKeys = Object.keys(payload);
             modelType = modelTypeKeys.length ? modelTypeKeys.firstObject : false;
@@ -187,11 +187,11 @@ export default class FetchService extends Service {
         const type = dasherize(singularize(modelType));
 
         if (isArray(payload)) {
-            return payload.map(instance => this.store.push(this.store.normalize(type, instance)));
+            return payload.map((instance) => this.store.push(this.store.normalize(type, instance)));
         }
 
         if (isArray(payload[modelType])) {
-            return payload[modelType].map(instance => this.store.push(this.store.normalize(type, instance)));
+            return payload[modelType].map((instance) => this.store.push(this.store.normalize(type, instance)));
         }
 
         if (!isBlank(payload) && isBlank(payload[modelType])) {
@@ -209,7 +209,7 @@ export default class FetchService extends Service {
      *
      * @return {Model}            An ember model
      */
-    jsonToModel (attributes = {}, modelType) {
+    jsonToModel(attributes = {}, modelType) {
         if (typeof attributes === 'string') {
             attributes = JSON.parse(attributes);
         }
@@ -228,7 +228,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    async parseJSON (response) {
+    async parseJSON(response) {
         try {
             const compressedHeader = await response.headers.get('x-compressed-json');
             let json;
@@ -265,7 +265,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    request (path, method = 'GET', data = {}, options = {}) {
+    request(path, method = 'GET', data = {}, options = {}) {
         const headers = Object.assign(this.getHeaders(), options.headers ?? {});
         const host = options.host ?? this.host;
         const namespace = options.namespace ?? this.namespace;
@@ -280,7 +280,7 @@ export default class FetchService extends Service {
                 ...data,
             })
                 .then(this.parseJSON)
-                .then(response => {
+                .then((response) => {
                     if (response.ok) {
                         if (options.normalizeToEmberData) {
                             const normalized = this.normalizeModel(response.json, options.normalizeModelType);
@@ -334,7 +334,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    get (path, query = {}, options = {}) {
+    get(path, query = {}, options = {}) {
         // handle if want to request from cache
         if (options.fromCache === true) {
             return this.cachedGet(...arguments);
@@ -355,13 +355,13 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    cachedGet (path, query = {}, options = {}) {
+    cachedGet(path, query = {}, options = {}) {
         const pathKey = dasherize(path);
         const pathKeyVersion = new Date().toISOString();
 
         const request = () => {
             delete options.fromCache;
-            return this.get(path, query, options).then(response => {
+            return this.get(path, query, options).then((response) => {
                 // cache the response
                 this.localCache.set(pathKey, response);
                 this.localCache.set(`${pathKey}-version`, pathKeyVersion);
@@ -373,7 +373,7 @@ export default class FetchService extends Service {
 
         // check to see if in storage already
         if (this.localCache.get(pathKey)) {
-            return new Promise(resolve => {
+            return new Promise((resolve) => {
                 // get cached data
                 const data = this.localCache.get(pathKey);
 
@@ -393,7 +393,7 @@ export default class FetchService extends Service {
                 // if the version is older than 3 days clear it
                 if (!version || shouldExpire || options.clearData === true) {
                     this.flushRequestCache(path);
-                    return request();
+                    return request().then(resolve);
                 }
 
                 if (options.normalizeToEmberData) {
@@ -414,7 +414,7 @@ export default class FetchService extends Service {
      *
      * @param {string} path - The path for which the cache should be flushed.
      */
-    flushRequestCache (path) {
+    flushRequestCache(path) {
         const pathKey = dasherize(path);
 
         this.localCache.set(pathKey, undefined);
@@ -426,7 +426,7 @@ export default class FetchService extends Service {
      * of the console with the cached version. If they differ, the cache is cleared
      * and the new version is saved.
      */
-    shouldResetCache () {
+    shouldResetCache() {
         const consoleVersion = this.localCache.get('console-version');
 
         if (!consoleVersion || consoleVersion !== config.APP.version) {
@@ -444,7 +444,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    post (path, data = {}, options = {}) {
+    post(path, data = {}, options = {}) {
         return this.request(path, 'POST', { body: JSON.stringify(data) }, options);
     }
 
@@ -457,7 +457,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    put (path, data = {}, options = {}) {
+    put(path, data = {}, options = {}) {
         return this.request(path, 'PUT', { body: JSON.stringify(data) }, options);
     }
 
@@ -470,7 +470,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    delete (path, data = {}, options = {}) {
+    delete(path, data = {}, options = {}) {
         return this.request(path, 'DELETE', { body: JSON.stringify(data) }, options);
     }
 
@@ -482,7 +482,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    patch (path, data = {}, options = {}) {
+    patch(path, data = {}, options = {}) {
         return this.request(path, 'PATCH', { body: JSON.stringify(data) }, options);
     }
 
@@ -495,9 +495,9 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    upload (path, files = [], options = {}) {
+    upload(path, files = [], options = {}) {
         const body = new FormData();
-        files.forEach(file => {
+        files.forEach((file) => {
             body.append('file', file);
         });
         return this.request(path, 'POST', { body }, options);
@@ -512,12 +512,12 @@ export default class FetchService extends Service {
      * @param {String} profile
      * @param {String} version
      */
-    routing (coordinates, query = {}, options = {}) {
+    routing(coordinates, query = {}, options = {}) {
         let service = options?.service ?? 'trip';
         let profile = options?.profile ?? 'driving';
         let version = options?.version ?? 'v1';
         let host = options?.host ?? `https://${options?.subdomain ?? 'routing'}.fleetbase.io`;
-        let route = coordinates.map(coords => coords.join(',')).join(';');
+        let route = coordinates.map((coords) => coords.join(',')).join(';');
         let params = !isEmptyObject(query) ? new URLSearchParams(query).toString() : '';
         let path = `${host}/${service}/${version}/${profile}/${route}`;
         let url = `${path}${params ? '?' + params : ''}`;
@@ -565,8 +565,8 @@ export default class FetchService extends Service {
                     withCredentials: true,
                     headers,
                 })
-                .then(response => response.json())
-                .catch(error => {
+                .then((response) => response.json())
+                .catch((error) => {
                     this.notifications.serverError(error, 'File upload failed.');
 
                     if (typeof errorCallback === 'function') {
@@ -608,7 +608,7 @@ export default class FetchService extends Service {
      *
      * @return {Promise}
      */
-    download (path, query = {}, options = {}) {
+    download(path, query = {}, options = {}) {
         const headers = Object.assign(this.getHeaders(), options.headers ?? {});
         const method = options.method ?? 'GET';
         const credentials = options.credentials ?? this.credentials;
@@ -629,7 +629,7 @@ export default class FetchService extends Service {
 
         return new Promise((resolve, reject) => {
             return fetch(`${baseUrl}/${path}${params}`, fetchOptions)
-                .then(response => {
+                .then((response) => {
                     options.fileName = this.getFilenameFromResponse(response, options.fileName);
                     options.mimeType = this.getMimeTypeFromResponse(response, options.mimeType);
 
@@ -639,15 +639,15 @@ export default class FetchService extends Service {
 
                     return response;
                 })
-                .then(response => response.blob())
-                .then(blob => resolve(download(blob, options.fileName, options.mimeType)))
-                .catch(error => {
+                .then((response) => response.blob())
+                .then((blob) => resolve(download(blob, options.fileName, options.mimeType)))
+                .catch((error) => {
                     reject(error);
                 });
         });
     }
 
-    getFilenameFromResponse (response, defaultFilename = null) {
+    getFilenameFromResponse(response, defaultFilename = null) {
         const contentDisposition = response.headers.get('content-disposition');
         let fileName = defaultFilename;
 
@@ -665,7 +665,7 @@ export default class FetchService extends Service {
         return fileName;
     }
 
-    getMimeTypeFromResponse (response, defaultMimeType = null) {
+    getMimeTypeFromResponse(response, defaultMimeType = null) {
         const contentType = response.headers.get('content-type');
         let mimeType = defaultMimeType;
 
@@ -680,10 +680,10 @@ export default class FetchService extends Service {
         return mimeType;
     }
 
-    fetchOrderConfigurations (params = {}) {
+    fetchOrderConfigurations(params = {}) {
         return new Promise((resolve, reject) => {
             this.request('fleet-ops/order-configs/get-installed', params)
-                .then(configs => {
+                .then((configs) => {
                     const serialized = [];
 
                     for (let i = 0; i < configs.length; i++) {
@@ -696,7 +696,7 @@ export default class FetchService extends Service {
 
                     resolve(serialized);
                 })
-                .catch(error => {
+                .catch((error) => {
                     reject(error);
                 });
         });

From 2c61c2d68383c6dd4d87f8b8c4ae91da2a1a4315 Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson" <me@ron.dev>
Date: Sat, 28 Sep 2024 22:20:14 +0800
Subject: [PATCH 4/5] added a post boot callback for engine boots

---
 addon/services/universe.js | 71 ++++++++++++++++++++++++++++++++------
 1 file changed, 60 insertions(+), 11 deletions(-)

diff --git a/addon/services/universe.js b/addon/services/universe.js
index e9f71a2..14606db 100644
--- a/addon/services/universe.js
+++ b/addon/services/universe.js
@@ -42,6 +42,7 @@ export default class UniverseService extends Service.extend(Evented) {
         widgets: A([]),
     };
     @tracked hooks = {};
+    @tracked bootCallbacks = A([]);
 
     /**
      * Computed property that returns all administrative menu items.
@@ -1734,7 +1735,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) {
+    async bootEngines(owner = null) {
         const booted = [];
         const pending = [];
         const additionalCoreExtensions = config.APP.extensions ?? [];
@@ -1748,7 +1749,7 @@ export default class UniverseService extends Service.extend(Evented) {
         this.applicationInstance = owner;
 
         const tryBootEngine = (extension) => {
-            this.loadEngine(extension.name).then((engineInstance) => {
+            return this.loadEngine(extension.name).then((engineInstance) => {
                 if (engineInstance.base && engineInstance.base.setupExtension) {
                     if (this.bootedExtensions.includes(extension.name)) {
                         return;
@@ -1803,11 +1804,13 @@ export default class UniverseService extends Service.extend(Evented) {
             pending.push(...stillPending);
         };
 
-        return loadInstalledExtensions(additionalCoreExtensions).then((extensions) => {
-            extensions.forEach((extension) => {
-                tryBootEngine(extension);
-            });
+        return loadInstalledExtensions(additionalCoreExtensions).then(async (extensions) => {
+            for (let i = 0; i < extensions.length; i++) {
+                const extension = extensions[i];
+                await tryBootEngine(extension);
+            }
 
+            this.runBootCallbacks(owner);
             this.enginesBooted = true;
         });
     }
@@ -1838,7 +1841,7 @@ export default class UniverseService extends Service.extend(Evented) {
         this.applicationInstance = owner;
 
         const tryBootEngine = (extension) => {
-            this.loadEngine(extension.name).then((engineInstance) => {
+            return this.loadEngine(extension.name).then((engineInstance) => {
                 if (engineInstance.base && engineInstance.base.setupExtension) {
                     const engineDependencies = getWithDefault(engineInstance.base, 'engineDependencies', []);
 
@@ -1887,11 +1890,13 @@ export default class UniverseService extends Service.extend(Evented) {
             pending.push(...stillPending);
         };
 
-        return loadExtensions().then((extensions) => {
-            extensions.forEach((extension) => {
-                tryBootEngine(extension);
-            });
+        return loadExtensions().then(async (extensions) => {
+            for (let i = 0; i < extensions.length; i++) {
+                const extension = extensions[i];
+                await tryBootEngine(extension);
+            }
 
+            this.runBootCallbacks(owner);
             this.enginesBooted = true;
         });
     }
@@ -1907,6 +1912,50 @@ export default class UniverseService extends Service.extend(Evented) {
         return this.bootedExtensions.includes(name);
     }
 
+    /**
+     * Registers a callback function to be executed after the engine boot process completes.
+     *
+     * This method ensures that the `bootCallbacks` array is initialized. It then adds the provided
+     * callback to this array. The callbacks registered will be invoked in sequence after the engine
+     * has finished booting, using the `runBootCallbacks` method.
+     *
+     * @param {Function} callback - The function to execute after the engine boots.
+     *   The callback should accept two arguments:
+     *   - `{Object} universe` - The universe context or environment.
+     *   - `{Object} appInstance` - The application instance.
+     */
+    afterBoot(callback) {
+        if (!isArray(this.bootCallbacks)) {
+            this.bootCallbacks = [];
+        }
+
+        this.bootCallbacks.pushObject(callback);
+    }
+
+    /**
+     * Executes all registered engine boot callbacks in the order they were added.
+     *
+     * This method iterates over the `bootCallbacks` array and calls each callback function,
+     * passing in the `universe` and `appInstance` parameters. After all callbacks have been
+     * executed, it optionally calls a completion function `onComplete`.
+     *
+     * @param {Object} appInstance - The application instance to pass to each callback.
+     * @param {Function} [onComplete] - Optional. A function to call after all boot callbacks have been executed.
+     *   It does not receive any arguments.
+     */
+    runBootCallbacks(appInstance, onComplete = null) {
+        for (let i = 0; i < this.bootCallbacks.length; i++) {
+            const callback = this.bootCallbacks[i];
+            if (typeof callback === 'function') {
+                callback(this, appInstance);
+            }
+        }
+
+        if (typeof onComplete === 'function') {
+            onComplete();
+        }
+    }
+
     /**
      * Alias for intl service `t`
      *

From f0bedd3b5f8024ed4b317ba57322a20de3ce4c02 Mon Sep 17 00:00:00 2001
From: "Ronald A. Richardson" <me@ron.dev>
Date: Tue, 1 Oct 2024 10:49:48 +0800
Subject: [PATCH 5/5] improvements to current user service, universe service,
 and url search params service, and fixed child service injection for
 engineService decorator

---
 addon/services/current-user.js       |   2 +
 addon/services/universe.js           |  61 +++++++-
 addon/services/url-search-params.js  | 224 ++++++++++++++++++++++-----
 addon/utils/inject-engine-service.js |   9 +-
 4 files changed, 257 insertions(+), 39 deletions(-)

diff --git a/addon/services/current-user.js b/addon/services/current-user.js
index 0a36343..defc226 100644
--- a/addon/services/current-user.js
+++ b/addon/services/current-user.js
@@ -32,6 +32,8 @@ export default class CurrentUserService extends Service.extend(Evented) {
     @alias('user.is_admin') isAdmin;
     @alias('user.company_uuid') companyId;
     @alias('user.company_name') companyName;
+    @alias('user.role_name') roleName;
+    @alias('user.role') role;
 
     @computed('id') get optionsPrefix() {
         return `${this.id}:`;
diff --git a/addon/services/universe.js b/addon/services/universe.js
index 14606db..f45443e 100644
--- a/addon/services/universe.js
+++ b/addon/services/universe.js
@@ -19,6 +19,7 @@ import config from 'ember-get-config';
 export default class UniverseService extends Service.extend(Evented) {
     @service router;
     @service intl;
+    @service urlSearchParams;
     @tracked applicationInstance;
     @tracked enginesBooted = false;
     @tracked bootedExtensions = A([]);
@@ -136,6 +137,17 @@ export default class UniverseService extends Service.extend(Evented) {
         return this.router.transitionTo(route, ...args);
     }
 
+    /**
+     * Sets the application instance.
+     *
+     * @param {ApplicationInstance} - The application instance object.
+     * @return {void}
+     */
+    setApplicationInstance(instance) {
+        window.Fleetbase = instance;
+        this.applicationInstance = instance;
+    }
+
     /**
      * Retrieves the application instance.
      *
@@ -262,6 +274,43 @@ export default class UniverseService extends Service.extend(Evented) {
         return this.router.transitionTo(route, slug);
     }
 
+    /**
+     * Redirects to a virtual route if a corresponding menu item exists based on the current URL slug.
+     *
+     * This asynchronous function checks whether a virtual route exists by extracting the slug from the current
+     * window's pathname and looking up a matching menu item in a specified registry. If a matching menu item
+     * is found, it initiates a transition to the given route associated with that menu item and returns the
+     * transition promise.
+     *
+     * @async
+     *
+     * @param {Object} transition - The current transition object from the router.
+     *   Used to retrieve additional information required for the menu item lookup.
+     * @param {string} registryName - The name of the registry to search for the menu item.
+     *   This registry should contain menu items mapped by their slugs.
+     * @param {string} route - The name of the route to transition to if the menu item is found.
+     *   This is typically the route associated with displaying the menu item's content.
+     *
+     * @returns {Promise|undefined} - Returns a promise that resolves when the route transition completes
+     *   if a matching menu item is found. If no matching menu item is found, the function returns undefined.
+     *
+     */
+    async virtualRouteRedirect(transition, registryName, route, options = {}) {
+        const view = this.getViewFromTransition(transition);
+        const slug = window.location.pathname.replace('/', '');
+        const queryParams = this.urlSearchParams.all();
+        const menuItem = await 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;
+            });
+        }
+    }
+
     /**
      * @action
      * Creates a new registry with the given name and options.
@@ -1396,6 +1445,14 @@ export default class UniverseService extends Service.extend(Evented) {
             isLoading,
         };
 
+        // make the menu item and universe object a default param of the onClick handler
+        if (typeof onClick === 'function') {
+            const universe = this;
+            menuItem.onClick = function () {
+                return onClick(menuItem, universe);
+            };
+        }
+
         return menuItem;
     }
 
@@ -1746,7 +1803,7 @@ export default class UniverseService extends Service.extend(Evented) {
         }
 
         // Set application instance
-        this.applicationInstance = owner;
+        this.setApplicationInstance(owner);
 
         const tryBootEngine = (extension) => {
             return this.loadEngine(extension.name).then((engineInstance) => {
@@ -1838,7 +1895,7 @@ export default class UniverseService extends Service.extend(Evented) {
         }
 
         // Set application instance
-        this.applicationInstance = owner;
+        this.setApplicationInstance(owner);
 
         const tryBootEngine = (extension) => {
             return this.loadEngine(extension.name).then((engineInstance) => {
diff --git a/addon/services/url-search-params.js b/addon/services/url-search-params.js
index f37c751..6beef64 100644
--- a/addon/services/url-search-params.js
+++ b/addon/services/url-search-params.js
@@ -1,35 +1,35 @@
 import Service from '@ember/service';
-import { tracked } from '@glimmer/tracking';
+import { debounce } from '@ember/runloop';
 import hasJsonStructure from '../utils/has-json-structure';
 
+/**
+ * Service for manipulating URL search parameters.
+ *
+ * This service provides methods to get, set, remove, and check URL query parameters.
+ * It also allows updating the browser's URL without reloading the page.
+ *
+ * @extends Service
+ */
 export default class UrlSearchParamsService extends Service {
     /**
-     * The active URL params
+     * Getter for `urlParams` that ensures it's always up-to-date with the current URL.
      *
-     * @var {Array}
+     * @type {URLSearchParams}
+     * @private
      */
-    @tracked urlParams;
-
-    /**
-     * Update the URL params
-     *
-     * @void
-     */
-    setSearchParams() {
-        this.urlParams = new URLSearchParams(window.location.search);
-
-        return this;
+    get urlParams() {
+        return new URLSearchParams(window.location.search);
     }
 
     /**
-     * Get a param
+     * Retrieves the value of a specific query parameter.
      *
-     * @param {String} key the url param
-     * @return mixed
+     * If the parameter value is a JSON string, it will be parsed into an object or array.
+     *
+     * @param {string} key - The name of the query parameter to retrieve.
+     * @returns {*} The value of the query parameter, parsed from JSON if applicable, or null if not found.
      */
     getParam(key) {
-        this.setSearchParams();
-
         let value = this.urlParams.get(key);
 
         if (hasJsonStructure(value)) {
@@ -40,47 +40,102 @@ export default class UrlSearchParamsService extends Service {
     }
 
     /**
-     * Get a param
+     * Sets or updates a query parameter in the URL search parameters.
+     *
+     * If the value is an object or array, it will be stringified to JSON.
+     *
+     * @param {string} key - The name of the query parameter to set.
+     * @param {*} value - The value of the query parameter.
+     * @returns {this} Returns the service instance for chaining.
+     */
+    setParam(key, value) {
+        if (typeof value === 'object') {
+            value = JSON.stringify(value);
+        } else {
+            value = encodeURIComponent(value);
+        }
+
+        this.urlParams.set(key, value);
+
+        return this;
+    }
+
+    /**
+     * Alias for `getParam`.
      *
-     * @param {String} key the url param
-     * @return mixed
+     * @param {string} key - The name of the query parameter to retrieve.
+     * @returns {*} The value of the query parameter.
      */
     get(key) {
         return this.getParam(key);
     }
 
     /**
-     * Determines if a queryParam exists
+     * Sets or updates a query parameter with multiple values.
      *
-     * @param {String} key the url param
-     * @var {Boolean}
+     * @param {string} key - The name of the query parameter to set.
+     * @param {Array} values - An array of values for the parameter.
+     * @returns {this} Returns the service instance for chaining.
+     */
+    setParamArray(key, values) {
+        this.urlParams.delete(key);
+        values.forEach((value) => {
+            this.urlParams.append(key, value);
+        });
+
+        return this;
+    }
+
+    /**
+     * Retrieves all values of a specific query parameter.
+     *
+     * @param {string} key - The name of the query parameter.
+     * @returns {Array} An array of values for the parameter.
+     */
+    getParamArray(key) {
+        return this.urlParams.getAll(key);
+    }
+
+    /**
+     * Checks if a specific query parameter exists in the URL.
+     *
+     * @param {string} key - The name of the query parameter to check.
+     * @returns {boolean} True if the parameter exists, false otherwise.
      */
     exists(key) {
-        this.setSearchParams();
+        return this.urlParams.has(key);
+    }
 
+    /**
+     * Checks if a specific query parameter has in the URL.
+     *
+     * @param {string} key - The name of the query parameter to check.
+     * @returns {boolean} True if the parameter exists, false otherwise.
+     */
+    has(key) {
         return this.urlParams.has(key);
     }
 
     /**
-     * Remove a queryparam
+     * Removes a specific query parameter from the URL search parameters.
      *
-     * @param {String} key the url param
-     * @void
+     * @param {string} key - The name of the query parameter to remove.
+     * @returns {this} Returns the service instance for chaining.
      */
     remove(key) {
-        this.setSearchParams();
+        this.urlParams.delete(key);
 
-        return this.urlParams.delete(key);
+        return this;
     }
 
     /**
-     * Returns object of all params
+     * Retrieves all query parameters as an object.
      *
-     * @return {Array}
+     * Each parameter value is processed by `getParam`, which parses JSON values if applicable.
+     *
+     * @returns {Object} An object containing all query parameters and their values.
      */
     all() {
-        this.setSearchParams();
-
         const all = {};
 
         for (let key of this.urlParams.keys()) {
@@ -89,4 +144,103 @@ export default class UrlSearchParamsService extends Service {
 
         return all;
     }
+
+    /**
+     * Updates the browser's URL with the current `urlParams` without reloading the page.
+     *
+     * @returns {void}
+     */
+    updateUrl() {
+        const url = new URL(window.location.href);
+        url.search = this.urlParams.toString();
+        window.history.pushState({ path: url.href }, '', url.href);
+    }
+
+    /**
+     * Updates the browser's URL with the current `urlParams`, debounced to prevent excessive calls.
+     *
+     * @returns {void}
+     */
+    updateUrlDebounced() {
+        debounce(this, this.updateUrl, 100);
+    }
+
+    /**
+     * Clears all query parameters from the URL search parameters.
+     *
+     * @returns {this} Returns the service instance for chaining.
+     */
+    clear() {
+        this.urlParams = new URLSearchParams();
+
+        return this;
+    }
+
+    /**
+     * Returns the full URL as a string with the current `urlParams`.
+     *
+     * @returns {string} The full URL with updated query parameters.
+     */
+    getFullUrl() {
+        const url = new URL(window.location.href);
+        url.search = this.urlParams.toString();
+
+        return url.toString();
+    }
+
+    /**
+     * Returns the current path with the updated query parameters.
+     *
+     * @returns {string} The path and search portion of the URL.
+     */
+    getPathWithParams() {
+        return `${window.location.pathname}?${this.urlParams.toString()}`;
+    }
+
+    /**
+     * Removes a query parameter from the current URL and updates the browser history.
+     *
+     * This method modifies the browser's URL by removing the specified parameter and uses the History API
+     * to update the URL without reloading the page.
+     *
+     * @param {string} paramToRemove - The name of the query parameter to remove from the URL.
+     * @returns {void}
+     */
+    removeParamFromCurrentUrl(paramToRemove) {
+        const url = new URL(window.location.href);
+        url.searchParams.delete(paramToRemove);
+        window.history.pushState({ path: url.href }, '', url.href);
+    }
+
+    /**
+     * Adds or updates a query parameter in the current URL and updates the browser history.
+     *
+     * This method modifies the browser's URL by adding or updating the specified parameter and uses the History API
+     * to update the URL without reloading the page.
+     *
+     * @param {string} paramName - The name of the query parameter to add or update.
+     * @param {string} paramValue - The value of the query parameter.
+     * @returns {void}
+     */
+    addParamToCurrentUrl(paramName, paramValue) {
+        const url = new URL(window.location.href);
+        url.searchParams.set(paramName, paramValue);
+        window.history.pushState({ path: url.href }, '', url.href);
+    }
+
+    /**
+     * Adds or updates a query parameters from a provided object into the current URL and updates the browser history.
+     *
+     * This method modifies the browser's URL by adding or updating the specified parameters and uses the History API
+     * to update the URL without reloading the page.
+     *
+     * @param {Object} params - The query parameters to add or update.
+     * @returns {void}
+     */
+    setParamsToCurrentUrl(params = {}) {
+        for (let param in params) {
+            const value = params[param];
+            this.addParamToCurrentUrl(param, value);
+        }
+    }
 }
diff --git a/addon/utils/inject-engine-service.js b/addon/utils/inject-engine-service.js
index cdb7ebb..721e0d5 100644
--- a/addon/utils/inject-engine-service.js
+++ b/addon/utils/inject-engine-service.js
@@ -1,10 +1,11 @@
 import { getOwner } from '@ember/application';
 import { isArray } from '@ember/array';
+import Service from '@ember/service';
 import isObject from './is-object';
 
 function findService(owner, target, serviceName) {
     let service = target[serviceName];
-    if (!service) {
+    if (!(service instanceof Service)) {
         service = owner.lookup(`service:${serviceName}`);
     }
 
@@ -33,8 +34,12 @@ function automaticServiceResolution(service, target, owner) {
     }
 }
 
+function _getOwner(target) {
+    return window.Fleetbase ?? getOwner(target);
+}
+
 export default function injectEngineService(target, engineName, serviceName, options = {}) {
-    const owner = getOwner(target);
+    const owner = _getOwner(target);
     const universe = owner.lookup('service:universe');
     const service = universe.getServiceFromEngine(engineName, serviceName);
     const key = options.key || null;