From a795b34e4c5bed77ea1d2b3c403584461e04a40c Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 9 Oct 2026 10:54:09 +0800 Subject: [PATCH 01/10] fix(serializers): customer contacts embed their places so a place type is not read as a model --- RELEASE.md | 5 ++--- addon/serializers/customer-contact.js | 8 ++++++++ app/serializers/customer-contact.js | 1 + package.json | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 addon/serializers/customer-contact.js create mode 100644 app/serializers/customer-contact.js diff --git a/RELEASE.md b/RELEASE.md index 0e0715d..de48233 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,10 +1,9 @@ -> v0.2.2 ~ "Managed login state" +> v0.2.3 ~ "Customer places load" --- ## Highlights -- **Driver and contact models expose their login state.** New read-only `is_staff_linked` and `login_status` attributes let the console show Reset Password, Send Credentials and Deactivate/Reactivate Login. They also lock email and phone on a profile linked to a team member's account. -- **The login account is never sent back.** Driver and contact login accounts are now managed by the server from the profile's name, email and phone (fleetbase/fleetops#338). The driver serializer no longer sends `user_uuid` or the login fields. The contact serializer no longer sends `user`, `user_uuid` or the login fields. +- **An order's customer loads with its saved places.** Customer contacts now have their own serializer, so their embedded `place` and `places` load as `place` records. Before, a place with a type such as "apartment" or "house" was read as a model name, and the order failed to load with "No model was found for 'apartment'". --- ## Need help? diff --git a/addon/serializers/customer-contact.js b/addon/serializers/customer-contact.js new file mode 100644 index 0000000..58912b7 --- /dev/null +++ b/addon/serializers/customer-contact.js @@ -0,0 +1,8 @@ +import CustomerSerializer from './customer'; + +/** + * A contact loaded as an order's customer. It shares the contact serializer so + * its embedded `place` and `places` resolve to the declared `place` model rather + * than being looked up by each place's own `type` attribute (apartment, house, office...). + */ +export default class CustomerContactSerializer extends CustomerSerializer {} diff --git a/app/serializers/customer-contact.js b/app/serializers/customer-contact.js new file mode 100644 index 0000000..c94f956 --- /dev/null +++ b/app/serializers/customer-contact.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/serializers/customer-contact'; diff --git a/package.json b/package.json index a466422..527592d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fleetbase/fleetops-data", - "version": "0.2.2", + "version": "0.2.3", "description": "Fleetbase Fleet-Ops based models, serializers, transforms, adapters and GeoJson utility functions.", "keywords": [ "fleetbase-data", From 5914e855dd453670cb03b95331388d7a7682f973 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 9 Oct 2026 11:37:06 +0800 Subject: [PATCH 02/10] fix(serializers): customer vendors embed their places too --- RELEASE.md | 2 +- addon/serializers/customer-vendor.js | 7 +++++++ app/serializers/customer-vendor.js | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 addon/serializers/customer-vendor.js create mode 100644 app/serializers/customer-vendor.js diff --git a/RELEASE.md b/RELEASE.md index de48233..d5f7a47 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -3,7 +3,7 @@ --- ## Highlights -- **An order's customer loads with its saved places.** Customer contacts now have their own serializer, so their embedded `place` and `places` load as `place` records. Before, a place with a type such as "apartment" or "house" was read as a model name, and the order failed to load with "No model was found for 'apartment'". +- **An order's customer loads with its saved places.** Customer contacts and customer vendors now have their own serializers, so their embedded `place` and `places` load as `place` records. Before, a place with a type such as "apartment" or "house" was read as a model name, and the order failed to load with "No model was found for 'apartment'". --- ## Need help? diff --git a/addon/serializers/customer-vendor.js b/addon/serializers/customer-vendor.js new file mode 100644 index 0000000..b2174e9 --- /dev/null +++ b/addon/serializers/customer-vendor.js @@ -0,0 +1,7 @@ +import CustomerSerializer from './customer'; + +/** + * A vendor loaded as an order's customer. Like customer contacts, its embedded + * `place` and `places` are normalized as the declared `place` model. + */ +export default class CustomerVendorSerializer extends CustomerSerializer {} diff --git a/app/serializers/customer-vendor.js b/app/serializers/customer-vendor.js new file mode 100644 index 0000000..c4fd2cf --- /dev/null +++ b/app/serializers/customer-vendor.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/serializers/customer-vendor'; From d5e74abbccfdecc81a2cb419e49d6e4f3673efd4 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 9 Oct 2026 16:53:12 +0800 Subject: [PATCH 03/10] feat(descriptors): share the driver, vehicle, customer, contact, place, order, vendor and fleet identity descriptors with every extension The resource identity descriptors for the resources every extension can see through this package's models now live here, together with the thin pill, summary, identity-cell and select-option wrappers that name the resource type for ember-ui's generic components, and the styled placeholder silhouettes they fall back to. An instance initializer registers the descriptors whenever the host or an engine boots, filling in only keys nobody has registered, so a Storefront or Ledger table renders a driver as an identity cell on a deep link without loading the FleetOps engine. Opening a resource loads FleetOps on demand and hands the record to its action service, so the engine bundle is only fetched on click. FleetOps replaces these with its own openers when it boots. Covered at 100% by unit tests for the helpers, descriptors, placeholder images and initializer, and a rendering test that proves every wrapper resolves and passes its resource type through. --- README.md | 21 +- addon/components/cell/contact-identity.hbs | 1 + addon/components/cell/customer-identity.hbs | 1 + addon/components/cell/driver-identity.hbs | 1 + addon/components/cell/fleet-identity.hbs | 1 + addon/components/cell/order-identity.hbs | 1 + addon/components/cell/place-identity.hbs | 1 + addon/components/cell/vehicle-identity.hbs | 1 + addon/components/cell/vendor-identity.hbs | 1 + addon/components/contact/pill.hbs | 24 ++ addon/components/contact/summary.hbs | 9 + addon/components/customer/pill.hbs | 24 ++ addon/components/customer/summary.hbs | 9 + addon/components/driver/pill.hbs | 24 ++ addon/components/driver/summary.hbs | 9 + addon/components/fleet/pill.hbs | 24 ++ addon/components/fleet/summary.hbs | 9 + addon/components/order/pill.hbs | 24 ++ addon/components/order/summary.hbs | 9 + addon/components/place/pill.hbs | 24 ++ addon/components/place/summary.hbs | 9 + addon/components/select-option/contact.hbs | 12 + addon/components/select-option/customer.hbs | 12 + addon/components/select-option/driver.hbs | 12 + addon/components/select-option/fleet.hbs | 12 + addon/components/select-option/order.hbs | 12 + addon/components/select-option/place.hbs | 12 + addon/components/select-option/vehicle.hbs | 12 + addon/components/select-option/vendor.hbs | 12 + addon/components/vehicle/pill.hbs | 24 ++ addon/components/vehicle/summary.hbs | 9 + addon/components/vendor/pill.hbs | 24 ++ addon/components/vendor/summary.hbs | 9 + .../register-shared-resource-descriptors.js | 46 +++ addon/utils/placeholder-images.js | 71 +++++ addon/utils/resource-descriptors/helpers.js | 193 ++++++++++++ addon/utils/resource-descriptors/index.js | 2 + addon/utils/resource-descriptors/shared.js | 296 ++++++++++++++++++ app/components/cell/contact-identity.js | 1 + app/components/cell/customer-identity.js | 1 + app/components/cell/driver-identity.js | 1 + app/components/cell/fleet-identity.js | 1 + app/components/cell/order-identity.js | 1 + app/components/cell/place-identity.js | 1 + app/components/cell/vehicle-identity.js | 1 + app/components/cell/vendor-identity.js | 1 + app/components/contact/pill.js | 1 + app/components/contact/summary.js | 1 + app/components/customer/pill.js | 1 + app/components/customer/summary.js | 1 + app/components/driver/pill.js | 1 + app/components/driver/summary.js | 1 + app/components/fleet/pill.js | 1 + app/components/fleet/summary.js | 1 + app/components/order/pill.js | 1 + app/components/order/summary.js | 1 + app/components/place/pill.js | 1 + app/components/place/summary.js | 1 + app/components/select-option/contact.js | 1 + app/components/select-option/customer.js | 1 + app/components/select-option/driver.js | 1 + app/components/select-option/fleet.js | 1 + app/components/select-option/order.js | 1 + app/components/select-option/place.js | 1 + app/components/select-option/vehicle.js | 1 + app/components/select-option/vendor.js | 1 + app/components/vehicle/pill.js | 1 + app/components/vehicle/summary.js | 1 + app/components/vendor/pill.js | 1 + app/components/vendor/summary.js | 1 + .../register-shared-resource-descriptors.js | 1 + package.json | 2 +- .../components/resource-wrappers-test.js | 58 ++++ ...gister-shared-resource-descriptors-test.js | 98 ++++++ tests/unit/utils/placeholder-images-test.js | 63 ++++ .../resource-descriptors/helpers-test.js | 207 ++++++++++++ .../utils/resource-descriptors/shared-test.js | 270 ++++++++++++++++ 77 files changed, 1726 insertions(+), 2 deletions(-) create mode 100644 addon/components/cell/contact-identity.hbs create mode 100644 addon/components/cell/customer-identity.hbs create mode 100644 addon/components/cell/driver-identity.hbs create mode 100644 addon/components/cell/fleet-identity.hbs create mode 100644 addon/components/cell/order-identity.hbs create mode 100644 addon/components/cell/place-identity.hbs create mode 100644 addon/components/cell/vehicle-identity.hbs create mode 100644 addon/components/cell/vendor-identity.hbs create mode 100644 addon/components/contact/pill.hbs create mode 100644 addon/components/contact/summary.hbs create mode 100644 addon/components/customer/pill.hbs create mode 100644 addon/components/customer/summary.hbs create mode 100644 addon/components/driver/pill.hbs create mode 100644 addon/components/driver/summary.hbs create mode 100644 addon/components/fleet/pill.hbs create mode 100644 addon/components/fleet/summary.hbs create mode 100644 addon/components/order/pill.hbs create mode 100644 addon/components/order/summary.hbs create mode 100644 addon/components/place/pill.hbs create mode 100644 addon/components/place/summary.hbs create mode 100644 addon/components/select-option/contact.hbs create mode 100644 addon/components/select-option/customer.hbs create mode 100644 addon/components/select-option/driver.hbs create mode 100644 addon/components/select-option/fleet.hbs create mode 100644 addon/components/select-option/order.hbs create mode 100644 addon/components/select-option/place.hbs create mode 100644 addon/components/select-option/vehicle.hbs create mode 100644 addon/components/select-option/vendor.hbs create mode 100644 addon/components/vehicle/pill.hbs create mode 100644 addon/components/vehicle/summary.hbs create mode 100644 addon/components/vendor/pill.hbs create mode 100644 addon/components/vendor/summary.hbs create mode 100644 addon/instance-initializers/register-shared-resource-descriptors.js create mode 100644 addon/utils/placeholder-images.js create mode 100644 addon/utils/resource-descriptors/helpers.js create mode 100644 addon/utils/resource-descriptors/index.js create mode 100644 addon/utils/resource-descriptors/shared.js create mode 100644 app/components/cell/contact-identity.js create mode 100644 app/components/cell/customer-identity.js create mode 100644 app/components/cell/driver-identity.js create mode 100644 app/components/cell/fleet-identity.js create mode 100644 app/components/cell/order-identity.js create mode 100644 app/components/cell/place-identity.js create mode 100644 app/components/cell/vehicle-identity.js create mode 100644 app/components/cell/vendor-identity.js create mode 100644 app/components/contact/pill.js create mode 100644 app/components/contact/summary.js create mode 100644 app/components/customer/pill.js create mode 100644 app/components/customer/summary.js create mode 100644 app/components/driver/pill.js create mode 100644 app/components/driver/summary.js create mode 100644 app/components/fleet/pill.js create mode 100644 app/components/fleet/summary.js create mode 100644 app/components/order/pill.js create mode 100644 app/components/order/summary.js create mode 100644 app/components/place/pill.js create mode 100644 app/components/place/summary.js create mode 100644 app/components/select-option/contact.js create mode 100644 app/components/select-option/customer.js create mode 100644 app/components/select-option/driver.js create mode 100644 app/components/select-option/fleet.js create mode 100644 app/components/select-option/order.js create mode 100644 app/components/select-option/place.js create mode 100644 app/components/select-option/vehicle.js create mode 100644 app/components/select-option/vendor.js create mode 100644 app/components/vehicle/pill.js create mode 100644 app/components/vehicle/summary.js create mode 100644 app/components/vendor/pill.js create mode 100644 app/components/vendor/summary.js create mode 100644 app/instance-initializers/register-shared-resource-descriptors.js create mode 100644 tests/integration/components/resource-wrappers-test.js create mode 100644 tests/unit/instance-initializers/register-shared-resource-descriptors-test.js create mode 100644 tests/unit/utils/placeholder-images-test.js create mode 100644 tests/unit/utils/resource-descriptors/helpers-test.js create mode 100644 tests/unit/utils/resource-descriptors/shared-test.js diff --git a/README.md b/README.md index a45c828..5e878ef 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,26 @@ ember install @fleetbase/fleetops-data ## Usage -[Longer description of how to use the addon in apps.] +Install the addon and the Fleet-Ops models, adapters, serializers and transforms are available to the host application and to every engine that lists it as a dependency. + +### Shared resource descriptors + +The addon also ships the resource identity descriptors for the resources every extension can see through its models: `driver`, `vehicle`, `customer`, `contact`, `place`, `order`, `vendor` and `fleet`, along with the thin ``, ``, `` and `` wrappers that name the resource type for the generic components in `@fleetbase/ember-ui`. + +An instance initializer registers the descriptors with the `resource-registry` service whenever the host or an engine boots, filling in only the keys nobody has registered yet. That means a Storefront or Ledger table can render a driver as an identity cell before the Fleet-Ops engine has loaded: + +```js +{ + id: 'driver-assigned', + cellComponent: 'table/cell/identity', + resourceType: 'driver', + resourcePath: 'driver_assigned', +} +``` + +Opening a resource loads the Fleet-Ops engine on demand and hands the record to its action service, so the engine bundle is only fetched when someone clicks. When Fleet-Ops itself boots it replaces these descriptors with its own, which open the same panels directly. + +The helpers behind the descriptors and the styled placeholder images are exported from `@fleetbase/fleetops-data/utils/resource-descriptors` and `@fleetbase/fleetops-data/utils/placeholder-images` for other packages that build descriptors. ## Contributing diff --git a/addon/components/cell/contact-identity.hbs b/addon/components/cell/contact-identity.hbs new file mode 100644 index 0000000..3457db9 --- /dev/null +++ b/addon/components/cell/contact-identity.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addon/components/cell/customer-identity.hbs b/addon/components/cell/customer-identity.hbs new file mode 100644 index 0000000..e099808 --- /dev/null +++ b/addon/components/cell/customer-identity.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addon/components/cell/driver-identity.hbs b/addon/components/cell/driver-identity.hbs new file mode 100644 index 0000000..44f6282 --- /dev/null +++ b/addon/components/cell/driver-identity.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addon/components/cell/fleet-identity.hbs b/addon/components/cell/fleet-identity.hbs new file mode 100644 index 0000000..3706244 --- /dev/null +++ b/addon/components/cell/fleet-identity.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addon/components/cell/order-identity.hbs b/addon/components/cell/order-identity.hbs new file mode 100644 index 0000000..9c3940b --- /dev/null +++ b/addon/components/cell/order-identity.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addon/components/cell/place-identity.hbs b/addon/components/cell/place-identity.hbs new file mode 100644 index 0000000..f5cb82a --- /dev/null +++ b/addon/components/cell/place-identity.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addon/components/cell/vehicle-identity.hbs b/addon/components/cell/vehicle-identity.hbs new file mode 100644 index 0000000..77d5a71 --- /dev/null +++ b/addon/components/cell/vehicle-identity.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addon/components/cell/vendor-identity.hbs b/addon/components/cell/vendor-identity.hbs new file mode 100644 index 0000000..303c84e --- /dev/null +++ b/addon/components/cell/vendor-identity.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addon/components/contact/pill.hbs b/addon/components/contact/pill.hbs new file mode 100644 index 0000000..9e32991 --- /dev/null +++ b/addon/components/contact/pill.hbs @@ -0,0 +1,24 @@ + + {{yield resource}} + \ No newline at end of file diff --git a/addon/components/contact/summary.hbs b/addon/components/contact/summary.hbs new file mode 100644 index 0000000..56e167a --- /dev/null +++ b/addon/components/contact/summary.hbs @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/addon/components/customer/pill.hbs b/addon/components/customer/pill.hbs new file mode 100644 index 0000000..5e7da42 --- /dev/null +++ b/addon/components/customer/pill.hbs @@ -0,0 +1,24 @@ + + {{yield resource}} + \ No newline at end of file diff --git a/addon/components/customer/summary.hbs b/addon/components/customer/summary.hbs new file mode 100644 index 0000000..dbc7d25 --- /dev/null +++ b/addon/components/customer/summary.hbs @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/addon/components/driver/pill.hbs b/addon/components/driver/pill.hbs new file mode 100644 index 0000000..a540456 --- /dev/null +++ b/addon/components/driver/pill.hbs @@ -0,0 +1,24 @@ + + {{yield resource}} + \ No newline at end of file diff --git a/addon/components/driver/summary.hbs b/addon/components/driver/summary.hbs new file mode 100644 index 0000000..2bb542e --- /dev/null +++ b/addon/components/driver/summary.hbs @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/addon/components/fleet/pill.hbs b/addon/components/fleet/pill.hbs new file mode 100644 index 0000000..7a090fb --- /dev/null +++ b/addon/components/fleet/pill.hbs @@ -0,0 +1,24 @@ + + {{yield resource}} + \ No newline at end of file diff --git a/addon/components/fleet/summary.hbs b/addon/components/fleet/summary.hbs new file mode 100644 index 0000000..6581492 --- /dev/null +++ b/addon/components/fleet/summary.hbs @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/addon/components/order/pill.hbs b/addon/components/order/pill.hbs new file mode 100644 index 0000000..c71acca --- /dev/null +++ b/addon/components/order/pill.hbs @@ -0,0 +1,24 @@ + + {{yield resource}} + \ No newline at end of file diff --git a/addon/components/order/summary.hbs b/addon/components/order/summary.hbs new file mode 100644 index 0000000..377afb6 --- /dev/null +++ b/addon/components/order/summary.hbs @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/addon/components/place/pill.hbs b/addon/components/place/pill.hbs new file mode 100644 index 0000000..1a0c28f --- /dev/null +++ b/addon/components/place/pill.hbs @@ -0,0 +1,24 @@ + + {{yield resource}} + \ No newline at end of file diff --git a/addon/components/place/summary.hbs b/addon/components/place/summary.hbs new file mode 100644 index 0000000..25fb183 --- /dev/null +++ b/addon/components/place/summary.hbs @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/addon/components/select-option/contact.hbs b/addon/components/select-option/contact.hbs new file mode 100644 index 0000000..99423f7 --- /dev/null +++ b/addon/components/select-option/contact.hbs @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/addon/components/select-option/customer.hbs b/addon/components/select-option/customer.hbs new file mode 100644 index 0000000..8efec3d --- /dev/null +++ b/addon/components/select-option/customer.hbs @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/addon/components/select-option/driver.hbs b/addon/components/select-option/driver.hbs new file mode 100644 index 0000000..988eaef --- /dev/null +++ b/addon/components/select-option/driver.hbs @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/addon/components/select-option/fleet.hbs b/addon/components/select-option/fleet.hbs new file mode 100644 index 0000000..fb8a6d3 --- /dev/null +++ b/addon/components/select-option/fleet.hbs @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/addon/components/select-option/order.hbs b/addon/components/select-option/order.hbs new file mode 100644 index 0000000..b2001ca --- /dev/null +++ b/addon/components/select-option/order.hbs @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/addon/components/select-option/place.hbs b/addon/components/select-option/place.hbs new file mode 100644 index 0000000..67ae56a --- /dev/null +++ b/addon/components/select-option/place.hbs @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/addon/components/select-option/vehicle.hbs b/addon/components/select-option/vehicle.hbs new file mode 100644 index 0000000..1fa9a3f --- /dev/null +++ b/addon/components/select-option/vehicle.hbs @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/addon/components/select-option/vendor.hbs b/addon/components/select-option/vendor.hbs new file mode 100644 index 0000000..9f81f2c --- /dev/null +++ b/addon/components/select-option/vendor.hbs @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/addon/components/vehicle/pill.hbs b/addon/components/vehicle/pill.hbs new file mode 100644 index 0000000..13093a2 --- /dev/null +++ b/addon/components/vehicle/pill.hbs @@ -0,0 +1,24 @@ + + {{yield resource}} + \ No newline at end of file diff --git a/addon/components/vehicle/summary.hbs b/addon/components/vehicle/summary.hbs new file mode 100644 index 0000000..6abeea1 --- /dev/null +++ b/addon/components/vehicle/summary.hbs @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/addon/components/vendor/pill.hbs b/addon/components/vendor/pill.hbs new file mode 100644 index 0000000..38fa047 --- /dev/null +++ b/addon/components/vendor/pill.hbs @@ -0,0 +1,24 @@ + + {{yield resource}} + \ No newline at end of file diff --git a/addon/components/vendor/summary.hbs b/addon/components/vendor/summary.hbs new file mode 100644 index 0000000..933014d --- /dev/null +++ b/addon/components/vendor/summary.hbs @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/addon/instance-initializers/register-shared-resource-descriptors.js b/addon/instance-initializers/register-shared-resource-descriptors.js new file mode 100644 index 0000000..8d60c0b --- /dev/null +++ b/addon/instance-initializers/register-shared-resource-descriptors.js @@ -0,0 +1,46 @@ +import { buildSharedResourceDescriptors } from '../utils/resource-descriptors/shared'; + +/** + * Registers the shared FleetOps resource descriptors with the resource + * registry the UI package provides, so a driver, customer or place renders + * as an identity cell, pill or summary in any extension without that + * extension loading the FleetOps engine first. + * + * It runs for the host application and again for every engine that depends + * on this package, so it only fills in descriptors nobody has registered: + * FleetOps registers richer versions of the same keys when it boots, and + * a later engine must not replace them. A host without the registry service + * simply keeps plain text. + */ +export function initialize(owner) { + let registry; + + try { + registry = owner.lookup('service:resource-registry'); + } catch { + registry = null; + } + + if (!registry || typeof registry.registerDescriptors !== 'function') { + return; + } + + const missing = buildSharedResourceDescriptors(owner).filter((descriptor) => !isRegistered(registry, descriptor.key)); + + if (missing.length) { + registry.registerDescriptors(missing); + } +} + +function isRegistered(registry, key) { + try { + return Boolean(typeof registry.getDescriptor === 'function' ? registry.getDescriptor(key) : registry.descriptors?.some?.((descriptor) => descriptor.key === key)); + } catch { + return false; + } +} + +export default { + name: 'register-shared-resource-descriptors', + initialize, +}; diff --git a/addon/utils/placeholder-images.js b/addon/utils/placeholder-images.js new file mode 100644 index 0000000..28b8e36 --- /dev/null +++ b/addon/utils/placeholder-images.js @@ -0,0 +1,71 @@ +import config from 'ember-get-config'; + +/** + * Flat silhouettes shown wherever a resource has no photo, in one consistent style + * across the console. They are inline data URIs so the engine does not depend on a + * hosted asset; a console can override any of them through + * `defaultValues.placeholders.` in its environment config. + */ +export const PLACEHOLDER_IMAGES = { + trailer: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNDgwIiB3aWR0aD0iNjQwIiBoZWlnaHQ9IjQ4MCI+PGcgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48cmVjdCB4PSI4OCIgeT0iMTI4IiB3aWR0aD0iNDcwIiBoZWlnaHQ9IjE5NiIgcng9IjE2IiBmaWxsPSIjYmRiZGJkIiBzdHJva2U9IiNkZWRlZGUiIHN0cm9rZS13aWR0aD0iMTAiLz48cmVjdCB4PSIxMDgiIHk9IjE1MCIgd2lkdGg9IjQzMCIgaGVpZ2h0PSIxNTIiIHJ4PSI4IiBmaWxsPSIjYzljOWM5Ii8+PHBhdGggZD0iTTEyOCAxNzhoMzkwTTEyOCAyMjZoMzkwTTEyOCAyNzRoMzkwIiBzdHJva2U9IiNiZGJkYmQiIHN0cm9rZS13aWR0aD0iNiIvPjxyZWN0IHg9IjUyMiIgeT0iMTQ2IiB3aWR0aD0iMTAiIGhlaWdodD0iMTYwIiByeD0iNCIgZmlsbD0iI2IwYjBiMCIvPjxyZWN0IHg9Ijg4IiB5PSIzMjQiIHdpZHRoPSI0NzAiIGhlaWdodD0iMTgiIHJ4PSI2IiBmaWxsPSIjYTlhOWE5Ii8+PHJlY3QgeD0iMTU2IiB5PSIzNDIiIHdpZHRoPSIxNCIgaGVpZ2h0PSI1MiIgcng9IjQiIGZpbGw9IiNhOWE5YTkiLz48cmVjdCB4PSIxNDYiIHk9IjM5MiIgd2lkdGg9IjM0IiBoZWlnaHQ9IjEwIiByeD0iNCIgZmlsbD0iI2E5YTlhOSIvPjxyZWN0IHg9Ijg4IiB5PSIzNDIiIHdpZHRoPSI1MiIgaGVpZ2h0PSIxNiIgcng9IjYiIGZpbGw9IiNhOWE5YTkiLz48Y2lyY2xlIGN4PSI0MTIiIGN5PSIzNjYiIHI9IjQwIiBmaWxsPSIjOGY4ZjhmIiBzdHJva2U9IiNkZWRlZGUiIHN0cm9rZS13aWR0aD0iOCIvPjxjaXJjbGUgY3g9IjQxMiIgY3k9IjM2NiIgcj0iMTYiIGZpbGw9IiNkOWQ5ZDkiLz48Y2lyY2xlIGN4PSI1MDAiIGN5PSIzNjYiIHI9IjQwIiBmaWxsPSIjOGY4ZjhmIiBzdHJva2U9IiNkZWRlZGUiIHN0cm9rZS13aWR0aD0iOCIvPjxjaXJjbGUgY3g9IjUwMCIgY3k9IjM2NiIgcj0iMTYiIGZpbGw9IiNkOWQ5ZDkiLz48L2c+PC9zdmc+', + vehicle: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNDgwIiB3aWR0aD0iNjQwIiBoZWlnaHQ9IjQ4MCI+PGcgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDApIHNjYWxlKDEuMCkiIG9wYWNpdHk9IjEuMCI+PHJlY3QgeD0iMjE2IiB5PSIxMzIiIHdpZHRoPSIzNTAiIGhlaWdodD0iMTkyIiByeD0iMTQiIGZpbGw9IiNiZGJkYmQiIHN0cm9rZT0iI2RlZGVkZSIgc3Ryb2tlLXdpZHRoPSIxMCIvPjxyZWN0IHg9IjIzNiIgeT0iMTU0IiB3aWR0aD0iMzEwIiBoZWlnaHQ9IjE0OCIgcng9IjgiIGZpbGw9IiNjOWM5YzkiLz48cGF0aCBkPSJNMjU2IDE4MmgyNzBNMjU2IDIzMGgyNzBNMjU2IDI3OGgyNzAiIHN0cm9rZT0iI2JkYmRiZCIgc3Ryb2tlLXdpZHRoPSI2Ii8+PHBhdGggZD0iTTIxNiAzMjRWMTkwSDk2Yy0xMiAwLTIyIDgtMjYgMjBMNjAgMjQ0djgweiIgZmlsbD0iI2JkYmRiZCIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjEwIi8+PHBhdGggZD0iTTEwNCAyMDZoODh2NTJIODR6IiBmaWxsPSIjZDlkOWQ5Ii8+PHJlY3QgeD0iNjAiIHk9IjMyNCIgd2lkdGg9IjUwNiIgaGVpZ2h0PSIxOCIgcng9IjYiIGZpbGw9IiNhOWE5YTkiLz48cmVjdCB4PSI1NiIgeT0iMjkyIiB3aWR0aD0iMTgiIGhlaWdodD0iMzAiIHJ4PSI1IiBmaWxsPSIjZDlkOWQ5Ii8+PGNpcmNsZSBjeD0iMTUwIiBjeT0iMzY2IiByPSI0MCIgZmlsbD0iIzhmOGY4ZiIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjgiLz48Y2lyY2xlIGN4PSIxNTAiIGN5PSIzNjYiIHI9IjE2IiBmaWxsPSIjZDlkOWQ5Ii8+PGNpcmNsZSBjeD0iNDcwIiBjeT0iMzY2IiByPSI0MCIgZmlsbD0iIzhmOGY4ZiIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjgiLz48Y2lyY2xlIGN4PSI0NzAiIGN5PSIzNjYiIHI9IjE2IiBmaWxsPSIjZDlkOWQ5Ii8+PC9nPjwvZz48L3N2Zz4=', + driver: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNDgwIiB3aWR0aD0iNjQwIiBoZWlnaHQ9IjQ4MCI+PGcgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48cGF0aCBkPSJNMTEwIDQ4MGMwLTk2IDc2LTE2NiAyMTAtMTY2czIxMCA3MCAyMTAgMTY2eiIgZmlsbD0iI2JkYmRiZCIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjEwIi8+PHBhdGggZD0iTTI3MCAzMjJsNTAgNTQgNTAtNTQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2Q5ZDlkOSIgc3Ryb2tlLXdpZHRoPSI4Ii8+PGNpcmNsZSBjeD0iMzIwIiBjeT0iMjA0IiByPSI5MiIgZmlsbD0iI2JkYmRiZCIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjEwIi8+PHBhdGggZD0iTTIzMCAxODZjMC02MiA0Mi05NiA5MC05NnM5MCAzNCA5MCA5NnoiIGZpbGw9IiNhOWE5YTkiIHN0cm9rZT0iI2RlZGVkZSIgc3Ryb2tlLXdpZHRoPSIxMCIvPjxwYXRoIGQ9Ik0zMjAgOTZ2ODgiIHN0cm9rZT0iI2RlZGVkZSIgc3Ryb2tlLXdpZHRoPSI2Ii8+PGNpcmNsZSBjeD0iMzIwIiBjeT0iOTIiIHI9IjkiIGZpbGw9IiNhOWE5YTkiIHN0cm9rZT0iI2RlZGVkZSIgc3Ryb2tlLXdpZHRoPSI2Ii8+PHBhdGggZD0iTTIyMiAxODZoMTkwYzY2IDAgOTYgMTAgOTYgMjJzLTMwIDIyLTk2IDIySDIyMnoiIGZpbGw9IiNhOWE5YTkiIHN0cm9rZT0iI2RlZGVkZSIgc3Ryb2tlLXdpZHRoPSI4Ii8+PC9nPjwvc3ZnPg==', + contact: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNDgwIiB3aWR0aD0iNjQwIiBoZWlnaHQ9IjQ4MCI+PGcgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48cGF0aCBkPSJNMTEwIDQ4MGMwLTk2IDc2LTE2NiAyMTAtMTY2czIxMCA3MCAyMTAgMTY2eiIgZmlsbD0iI2JkYmRiZCIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjEwIi8+PHBhdGggZD0iTTI3MCAzMjJsNTAgNTQgNTAtNTQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2Q5ZDlkOSIgc3Ryb2tlLXdpZHRoPSI4Ii8+PGNpcmNsZSBjeD0iMzIwIiBjeT0iMTg4IiByPSI5NiIgZmlsbD0iI2JkYmRiZCIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjEwIi8+PC9nPjwvc3ZnPg==', + customer: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNDgwIiB3aWR0aD0iNjQwIiBoZWlnaHQ9IjQ4MCI+PGcgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48cGF0aCBkPSJNMTEwIDQ4MGMwLTk2IDc2LTE2NiAyMTAtMTY2czIxMCA3MCAyMTAgMTY2eiIgZmlsbD0iI2JkYmRiZCIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjEwIi8+PHBhdGggZD0iTTI3MCAzMjJsNTAgNTQgNTAtNTQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2Q5ZDlkOSIgc3Ryb2tlLXdpZHRoPSI4Ii8+PGNpcmNsZSBjeD0iMzIwIiBjeT0iMTg4IiByPSI5NiIgZmlsbD0iI2JkYmRiZCIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjEwIi8+PGNpcmNsZSBjeD0iNDkwIiBjeT0iMzkyIiByPSI2MCIgZmlsbD0iIzhmOGY4ZiIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjEwIi8+PHBhdGggZD0iTTQ1OCAzOTRsMjIgMjIgNDQtNDgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2Q5ZDlkOSIgc3Ryb2tlLXdpZHRoPSIxNCIvPjwvZz48L3N2Zz4=', + vendor: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNDgwIiB3aWR0aD0iNjQwIiBoZWlnaHQ9IjQ4MCI+PGcgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48cGF0aCBkPSJNNzIgMjAwTDMyMCA5NmwyNDggMTA0djI0SDcyeiIgZmlsbD0iI2E5YTlhOSIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjEwIi8+PHJlY3QgeD0iMTAwIiB5PSIyMjIiIHdpZHRoPSI0NDAiIGhlaWdodD0iMTkwIiByeD0iOCIgZmlsbD0iI2JkYmRiZCIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjEwIi8+PHJlY3QgeD0iMjcyIiB5PSIyOTIiIHdpZHRoPSI5NiIgaGVpZ2h0PSIxMjAiIHJ4PSI2IiBmaWxsPSIjOGY4ZjhmIiBzdHJva2U9IiNkZWRlZGUiIHN0cm9rZS13aWR0aD0iOCIvPjxyZWN0IHg9IjE0MCIgeT0iMjYyIiB3aWR0aD0iODgiIGhlaWdodD0iNjYiIHJ4PSI2IiBmaWxsPSIjZDlkOWQ5Ii8+PHJlY3QgeD0iNDEyIiB5PSIyNjIiIHdpZHRoPSI4OCIgaGVpZ2h0PSI2NiIgcng9IjYiIGZpbGw9IiNkOWQ5ZDkiLz48cGF0aCBkPSJNMTg0IDI2MnY2Nk0xNDAgMjk1aDg4TTQ1NiAyNjJ2NjZNNDEyIDI5NWg4OCIgc3Ryb2tlPSIjYmRiZGJkIiBzdHJva2Utd2lkdGg9IjYiLz48cmVjdCB4PSI2MCIgeT0iNDEyIiB3aWR0aD0iNTIwIiBoZWlnaHQ9IjE2IiByeD0iNiIgZmlsbD0iI2E5YTlhOSIvPjwvZz48L3N2Zz4=', + fleet: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNDgwIiB3aWR0aD0iNjQwIiBoZWlnaHQ9IjQ4MCI+PGcgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNTAgLTQwKSBzY2FsZSgwLjcyKSIgb3BhY2l0eT0iMC40NSI+PHJlY3QgeD0iMjE2IiB5PSIxMzIiIHdpZHRoPSIzNTAiIGhlaWdodD0iMTkyIiByeD0iMTQiIGZpbGw9IiNiZGJkYmQiIHN0cm9rZT0iI2RlZGVkZSIgc3Ryb2tlLXdpZHRoPSIxMCIvPjxyZWN0IHg9IjIzNiIgeT0iMTU0IiB3aWR0aD0iMzEwIiBoZWlnaHQ9IjE0OCIgcng9IjgiIGZpbGw9IiNjOWM5YzkiLz48cGF0aCBkPSJNMjU2IDE4MmgyNzBNMjU2IDIzMGgyNzBNMjU2IDI3OGgyNzAiIHN0cm9rZT0iI2JkYmRiZCIgc3Ryb2tlLXdpZHRoPSI2Ii8+PHBhdGggZD0iTTIxNiAzMjRWMTkwSDk2Yy0xMiAwLTIyIDgtMjYgMjBMNjAgMjQ0djgweiIgZmlsbD0iI2JkYmRiZCIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjEwIi8+PHBhdGggZD0iTTEwNCAyMDZoODh2NTJIODR6IiBmaWxsPSIjZDlkOWQ5Ii8+PHJlY3QgeD0iNjAiIHk9IjMyNCIgd2lkdGg9IjUwNiIgaGVpZ2h0PSIxOCIgcng9IjYiIGZpbGw9IiNhOWE5YTkiLz48cmVjdCB4PSI1NiIgeT0iMjkyIiB3aWR0aD0iMTgiIGhlaWdodD0iMzAiIHJ4PSI1IiBmaWxsPSIjZDlkOWQ5Ii8+PGNpcmNsZSBjeD0iMTUwIiBjeT0iMzY2IiByPSI0MCIgZmlsbD0iIzhmOGY4ZiIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjgiLz48Y2lyY2xlIGN4PSIxNTAiIGN5PSIzNjYiIHI9IjE2IiBmaWxsPSIjZDlkOWQ5Ii8+PGNpcmNsZSBjeD0iNDcwIiBjeT0iMzY2IiByPSI0MCIgZmlsbD0iIzhmOGY4ZiIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjgiLz48Y2lyY2xlIGN4PSI0NzAiIGN5PSIzNjYiIHI9IjE2IiBmaWxsPSIjZDlkOWQ5Ii8+PC9nPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgwIDEwKSBzY2FsZSgwLjgpIiBvcGFjaXR5PSIwLjciPjxyZWN0IHg9IjIxNiIgeT0iMTMyIiB3aWR0aD0iMzUwIiBoZWlnaHQ9IjE5MiIgcng9IjE0IiBmaWxsPSIjYmRiZGJkIiBzdHJva2U9IiNkZWRlZGUiIHN0cm9rZS13aWR0aD0iMTAiLz48cmVjdCB4PSIyMzYiIHk9IjE1NCIgd2lkdGg9IjMxMCIgaGVpZ2h0PSIxNDgiIHJ4PSI4IiBmaWxsPSIjYzljOWM5Ii8+PHBhdGggZD0iTTI1NiAxODJoMjcwTTI1NiAyMzBoMjcwTTI1NiAyNzhoMjcwIiBzdHJva2U9IiNiZGJkYmQiIHN0cm9rZS13aWR0aD0iNiIvPjxwYXRoIGQ9Ik0yMTYgMzI0VjE5MEg5NmMtMTIgMC0yMiA4LTI2IDIwTDYwIDI0NHY4MHoiIGZpbGw9IiNiZGJkYmQiIHN0cm9rZT0iI2RlZGVkZSIgc3Ryb2tlLXdpZHRoPSIxMCIvPjxwYXRoIGQ9Ik0xMDQgMjA2aDg4djUySDg0eiIgZmlsbD0iI2Q5ZDlkOSIvPjxyZWN0IHg9IjYwIiB5PSIzMjQiIHdpZHRoPSI1MDYiIGhlaWdodD0iMTgiIHJ4PSI2IiBmaWxsPSIjYTlhOWE5Ii8+PHJlY3QgeD0iNTYiIHk9IjI5MiIgd2lkdGg9IjE4IiBoZWlnaHQ9IjMwIiByeD0iNSIgZmlsbD0iI2Q5ZDlkOSIvPjxjaXJjbGUgY3g9IjE1MCIgY3k9IjM2NiIgcj0iNDAiIGZpbGw9IiM4ZjhmOGYiIHN0cm9rZT0iI2RlZGVkZSIgc3Ryb2tlLXdpZHRoPSI4Ii8+PGNpcmNsZSBjeD0iMTUwIiBjeT0iMzY2IiByPSIxNiIgZmlsbD0iI2Q5ZDlkOSIvPjxjaXJjbGUgY3g9IjQ3MCIgY3k9IjM2NiIgcj0iNDAiIGZpbGw9IiM4ZjhmOGYiIHN0cm9rZT0iI2RlZGVkZSIgc3Ryb2tlLXdpZHRoPSI4Ii8+PGNpcmNsZSBjeD0iNDcwIiBjeT0iMzY2IiByPSIxNiIgZmlsbD0iI2Q5ZDlkOSIvPjwvZz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDYwKSBzY2FsZSgwLjg4KSIgb3BhY2l0eT0iMS4wIj48cmVjdCB4PSIyMTYiIHk9IjEzMiIgd2lkdGg9IjM1MCIgaGVpZ2h0PSIxOTIiIHJ4PSIxNCIgZmlsbD0iI2JkYmRiZCIgc3Ryb2tlPSIjZGVkZWRlIiBzdHJva2Utd2lkdGg9IjEwIi8+PHJlY3QgeD0iMjM2IiB5PSIxNTQiIHdpZHRoPSIzMTAiIGhlaWdodD0iMTQ4IiByeD0iOCIgZmlsbD0iI2M5YzljOSIvPjxwYXRoIGQ9Ik0yNTYgMTgyaDI3ME0yNTYgMjMwaDI3ME0yNTYgMjc4aDI3MCIgc3Ryb2tlPSIjYmRiZGJkIiBzdHJva2Utd2lkdGg9IjYiLz48cGF0aCBkPSJNMjE2IDMyNFYxOTBIOTZjLTEyIDAtMjIgOC0yNiAyMEw2MCAyNDR2ODB6IiBmaWxsPSIjYmRiZGJkIiBzdHJva2U9IiNkZWRlZGUiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJNMTA0IDIwNmg4OHY1Mkg4NHoiIGZpbGw9IiNkOWQ5ZDkiLz48cmVjdCB4PSI2MCIgeT0iMzI0IiB3aWR0aD0iNTA2IiBoZWlnaHQ9IjE4IiByeD0iNiIgZmlsbD0iI2E5YTlhOSIvPjxyZWN0IHg9IjU2IiB5PSIyOTIiIHdpZHRoPSIxOCIgaGVpZ2h0PSIzMCIgcng9IjUiIGZpbGw9IiNkOWQ5ZDkiLz48Y2lyY2xlIGN4PSIxNTAiIGN5PSIzNjYiIHI9IjQwIiBmaWxsPSIjOGY4ZjhmIiBzdHJva2U9IiNkZWRlZGUiIHN0cm9rZS13aWR0aD0iOCIvPjxjaXJjbGUgY3g9IjE1MCIgY3k9IjM2NiIgcj0iMTYiIGZpbGw9IiNkOWQ5ZDkiLz48Y2lyY2xlIGN4PSI0NzAiIGN5PSIzNjYiIHI9IjQwIiBmaWxsPSIjOGY4ZjhmIiBzdHJva2U9IiNkZWRlZGUiIHN0cm9rZS13aWR0aD0iOCIvPjxjaXJjbGUgY3g9IjQ3MCIgY3k9IjM2NiIgcj0iMTYiIGZpbGw9IiNkOWQ5ZDkiLz48L2c+PC9nPjwvc3ZnPg==', +}; + +/** + * Hosted images the data models and console config fall back to when a record has no + * photo. Treat them as "no photo" so the styled placeholder wins over the legacy PNG. + */ +const LEGACY_DEFAULT_PATTERN = /(no-avatar|vehicle-placeholder|image-file-icon|fallback-placeholder|default-extension-icon)\.(png|svg|jpe?g)/i; + +const CONFIG_KEYS = { + vehicle: 'vehicleImage', + driver: 'driverImage', + vendor: 'vendorImage', + contact: 'contactImage', + customer: 'customerImage', + fleet: 'fleetImage', + trailer: 'trailerImage', +}; + +export function getPlaceholderImage(type = 'contact') { + const overrides = config?.defaultValues?.placeholders ?? {}; + // A console that already replaced its hosted defaults with inline images wins; + // hosted PNG defaults are ignored so the silhouette set stays consistent. + const configured = config?.defaultValues?.[CONFIG_KEYS[type]]; + const inlineConfigured = typeof configured === 'string' && configured.startsWith('data:') ? configured : null; + + return overrides[type] || inlineConfigured || PLACEHOLDER_IMAGES[type] || PLACEHOLDER_IMAGES.contact; +} + +export function isDefaultImage(url) { + if (typeof url !== 'string' || url.trim() === '') { + return true; + } + + if (LEGACY_DEFAULT_PATTERN.test(url)) { + return true; + } + + const defaults = Object.values(config?.defaultValues ?? {}).filter((value) => typeof value === 'string'); + + return defaults.includes(url); +} + +/** + * The image to show for a record: its own photo, or the styled placeholder when the + * photo is missing or one of the legacy hosted defaults. + */ +export function resolveResourceImage(url, type) { + return isDefaultImage(url) ? getPlaceholderImage(type) : url; +} + +export default getPlaceholderImage; diff --git a/addon/utils/resource-descriptors/helpers.js b/addon/utils/resource-descriptors/helpers.js new file mode 100644 index 0000000..07e88f7 --- /dev/null +++ b/addon/utils/resource-descriptors/helpers.js @@ -0,0 +1,193 @@ +import { get } from '@ember/object'; +import { format } from 'date-fns'; +import { getPlaceholderImage, resolveResourceImage } from '../placeholder-images'; + +/** + * Small building blocks the shared resource descriptors use. They are + * written against attribute names, so an identity stub or an API payload + * renders the same as an Ember Data record, and they never import from the + * UI package: whatever they need from it is looked up as a service. + */ + +export const FLEETOPS_ENGINE = '@fleetbase/fleetops-engine'; + +export function present(value) { + return value !== undefined && value !== null && String(value).trim() !== ''; +} + +/** The first present value at any of the paths. */ +export function first(record, ...paths) { + if (!record) { + return null; + } + + for (const path of paths) { + const value = get(record, path); + + if (present(value)) { + return value; + } + } + + return null; +} + +/** A service from the owner, or null when the owner cannot provide it. */ +export function lookupService(owner, name) { + try { + return owner?.lookup?.(`service:${name}`) ?? null; + } catch { + return null; + } +} + +/** A related record read through its reference, never a promise proxy. */ +export function relation(owner, record, name) { + const registry = lookupService(owner, 'resource-registry'); + + if (registry && typeof registry.relationValue === 'function') { + return registry.relationValue(record, name); + } + + const value = record ? get(record, name) : null; + + if (value && typeof value === 'object' && typeof value.then === 'function') { + return value.content ?? null; + } + + return value ?? null; +} + +/** A photo with the styled placeholder as fallback. */ +export function photo(record, type, path = 'photo_url') { + return { url: resolveResourceImage(get(record, path), type), fallback: getPlaceholderImage(type), shape: type === 'vehicle' || type === 'trailer' ? 'square' : 'round' }; +} + +export function icon(name, iconClass) { + return iconClass ? { icon: name, iconClass } : { icon: name }; +} + +export function badge(key, iconName, label, extra = {}) { + return present(label) ? { key, icon: iconName, label, ...extra } : null; +} + +export function badges(...list) { + return list.filter(Boolean); +} + +export function fact(label, value, extra = {}) { + return { labelKey: `resource-summary.facts.${label}`, value, ...extra }; +} + +/** A fact that renders as the related record's pill when the record is loaded. */ +export function relatedFact(label, related, relatedType, fallbackValue) { + return { labelKey: `resource-summary.facts.${label}`, related: related ?? null, relatedType, value: fallbackValue ?? null }; +} + +/** + * A date the way the console writes one everywhere else (date-fns, not the + * browser locale), or null for anything that is not a date. + */ +export function dateLabel(value, pattern = 'dd MMM yyyy, HH:mm') { + if (!present(value)) { + return null; + } + + const date = value instanceof Date ? value : new Date(value); + + return Number.isNaN(date.getTime()) ? null : format(date, pattern); +} + +/** + * A contact's type the way a person reads it: "customer-fleet-ops:contact" + * and "customer" both become "Customer"; anything unreadable becomes null. + */ +export function typeLabel(value) { + if (!present(value)) { + return null; + } + + const head = String(value) + .split(':')[0] + .replace(/-fleet-ops$/, '') + .replace(/[-_]+/g, ' ') + .trim(); + + return head ? head.charAt(0).toUpperCase() + head.slice(1) : null; +} + +export function money(amount, currency) { + if (!present(amount)) { + return null; + } + + return present(currency) ? `${amount} ${currency}` : String(amount); +} + +export function join(parts, separator = ' ') { + return parts.filter(present).join(separator) || null; +} + +/** The resolved `*_type` for a polymorphic relation, from the record or a `_type` attribute. */ +export function polymorphicType(record, relationName, typeAttr) { + const related = record ? get(record, relationName) : null; + const modelName = related?.constructor?.modelName ?? related?.content?.constructor?.modelName; + + return modelName ?? (typeAttr && record ? get(record, typeAttr) : null) ?? null; +} + +/** The universe extension manager, through either of its service names. */ +export function extensionManager(owner) { + return lookupService(owner, 'universe/extension-manager') ?? lookupService(owner, 'universe')?.extensionManager ?? null; +} + +/** Whether an engine is installed, so a resource it owns has somewhere to open. */ +export function engineInstalled(owner, engineName = FLEETOPS_ENGINE) { + const manager = extensionManager(owner); + + try { + return Boolean(manager?.isInstalled?.(engineName)); + } catch { + return false; + } +} + +/** + * An opener that loads an engine on demand, then hands the record to one of + * its action services: the context panel when it has one, else its route + * transition. The engine bundle is only fetched when someone actually clicks, + * never when a page that merely shows the resource renders. + */ +export function engineOpener(owner, serviceName, { engineName = FLEETOPS_ENGINE, mode = 'panel' } = {}) { + return async (record) => { + const manager = extensionManager(owner); + + if (!record || !manager || typeof manager.ensureEngineLoaded !== 'function') { + return false; + } + + let engine; + + try { + engine = await manager.ensureEngineLoaded(engineName); + } catch { + return false; + } + + const service = engine?.lookup?.(`service:${serviceName}`); + + if (!service) { + return false; + } + + const view = (mode === 'panel' ? (service.panel?.view ?? service.transition?.view) : null) ?? (mode === 'transition' ? service.transition?.view : null); + + if (typeof view !== 'function') { + return false; + } + + view.call(service, record); + + return true; + }; +} diff --git a/addon/utils/resource-descriptors/index.js b/addon/utils/resource-descriptors/index.js new file mode 100644 index 0000000..dd90d3e --- /dev/null +++ b/addon/utils/resource-descriptors/index.js @@ -0,0 +1,2 @@ +export { buildSharedResourceDescriptors, SHARED_RESOURCE_KEYS, guardOrder, default } from './shared'; +export * from './helpers'; diff --git a/addon/utils/resource-descriptors/shared.js b/addon/utils/resource-descriptors/shared.js new file mode 100644 index 0000000..9120409 --- /dev/null +++ b/addon/utils/resource-descriptors/shared.js @@ -0,0 +1,296 @@ +import { get } from '@ember/object'; +import { first, present, relation, photo, icon, badge, badges, fact, relatedFact, join, dateLabel, typeLabel, polymorphicType, engineOpener, engineInstalled } from './helpers'; + +/** + * The resources every Fleetbase extension can see through this package's + * models: drivers, vehicles, customers, contacts, places, orders, vendors and + * fleets. A descriptor teaches the console's resource identity components + * how to show one of them. Opening loads the FleetOps engine on demand, so a + * Storefront or Ledger page that lists drivers never pays for FleetOps until + * someone clicks one. FleetOps itself replaces these with its own openers + * when it boots. + */ +export const SHARED_RESOURCE_KEYS = ['driver', 'vehicle', 'customer', 'contact', 'place', 'order', 'vendor', 'fleet']; + +function onlineFlag(record) { + const online = get(record, 'online'); + + return typeof online === 'boolean' ? online : undefined; +} + +export function buildSharedResourceDescriptors(owner) { + const canOpen = () => engineInstalled(owner); + + return [ + { + key: 'driver', + labelKey: 'resource.driver', + icon: 'id-card', + modelNames: ['driver'], + aliases: ['attachable-driver', 'facilitator-driver', 'fleet-driver'], + polymorphicTypes: ['fleet-ops:driver', 'Fleetbase\\FleetOps\\Models\\Driver'], + permission: 'fleet-ops view driver', + statusTones: { + available: 'text-green-500', + active: 'text-green-500', + on_duty: 'text-green-500', + busy: 'text-yellow-500', + assigned: 'text-yellow-500', + unavailable: 'text-gray-400', + offline: 'text-gray-400', + suspended: 'text-red-500', + }, + title: (driver) => first(driver, 'name', 'displayName', 'display_name', 'public_id'), + identifier: (driver) => first(driver, 'phone', 'email'), + image: (driver) => photo(driver, 'driver'), + online: onlineFlag, + status: (driver) => first(driver, 'status'), + badges: (driver) => { + const vehicle = relation(owner, driver, 'vehicle'); + const label = first(vehicle, 'displayName', 'display_name', 'name') ?? first(driver, 'vehicle_assigned.display_name', 'vehicle_name'); + + return badges(badge('vehicle', 'car', label, { relatedType: 'vehicle', relatedId: vehicle?.id ?? get(driver, 'vehicle_uuid') })); + }, + selectDetails: (driver) => [first(driver, 'phone'), first(driver, 'email')], + facts: (driver) => [ + fact('phone', first(driver, 'phone')), + fact('email', first(driver, 'email')), + relatedFact('vehicle', relation(owner, driver, 'vehicle'), 'vehicle', first(driver, 'vehicle_name')), + fact( + 'licence', + join( + [ + first(driver, 'drivers_license_number'), + dateLabel(get(driver, 'license_expiry'), 'dd MMM yyyy') ? `expires ${dateLabel(get(driver, 'license_expiry'), 'dd MMM yyyy')}` : null, + ], + ' · ' + ) + ), + relatedFact('vendor', relation(owner, driver, 'vendor'), 'vendor', first(driver, 'vendor_name')), + fact('status', first(driver, 'status'), { format: 'humanize' }), + ], + canOpen, + open: engineOpener(owner, 'driver-actions'), + }, + { + key: 'vehicle', + labelKey: 'resource.vehicle', + icon: 'truck', + modelNames: ['vehicle'], + aliases: ['attachable-vehicle', 'maintenance-subject-vehicle'], + polymorphicTypes: ['fleet-ops:vehicle', 'Fleetbase\\FleetOps\\Models\\Vehicle'], + permission: 'fleet-ops view vehicle', + statusTones: { + available: 'text-green-500', + active: 'text-green-500', + in_service: 'text-green-500', + maintenance: 'text-yellow-500', + unavailable: 'text-gray-400', + inactive: 'text-gray-400', + out_of_service: 'text-red-500', + }, + title: (vehicle) => + first(vehicle, 'displayName', 'display_name', 'name', 'yearMakeModel', 'public_id') ?? join([get(vehicle, 'year'), get(vehicle, 'make'), get(vehicle, 'model')]), + identifier: (vehicle) => first(vehicle, 'plate_number', 'call_sign', 'vin', 'serial_number'), + image: (vehicle) => photo(vehicle, 'vehicle'), + online: onlineFlag, + status: (vehicle) => first(vehicle, 'status'), + badges: (vehicle) => { + const driver = relation(owner, vehicle, 'driver'); + + return badges( + badge('plate', 'id-card', first(vehicle, 'plate_number', 'call_sign', 'vehicle_number')), + badge('driver', 'user', first(driver, 'name', 'displayName') ?? first(vehicle, 'driver_name'), { + relatedType: 'driver', + relatedId: driver?.id ?? get(vehicle, 'driver_uuid'), + }) + ); + }, + selectDetails: (vehicle) => [first(vehicle, 'plate_number', 'vin', 'serial_number', 'call_sign'), first(vehicle, 'driver_name')], + facts: (vehicle) => { + const driver = relation(owner, vehicle, 'driver'); + const trailers = relation(owner, vehicle, 'trailers'); + const trailer = trailers ? (typeof trailers.objectAt === 'function' ? trailers.objectAt(0) : trailers[0]) : null; + const extra = trailers?.length > 1 ? ` (+${trailers.length - 1})` : ''; + + return [ + fact('make-model', join([get(vehicle, 'year'), get(vehicle, 'make'), get(vehicle, 'model'), get(vehicle, 'trim')])), + fact('plate', join([first(vehicle, 'plate_number'), first(vehicle, 'vin')], ' · ')), + relatedFact('driver', driver, 'driver', first(vehicle, 'driver_name')), + trailer ? { ...relatedFact('trailer', trailer, 'trailer', first(trailer, 'displayName', 'display_name', 'name')), suffix: extra } : fact('trailer', null), + fact('odometer', present(get(vehicle, 'odometer')) ? join([get(vehicle, 'odometer'), get(vehicle, 'odometer_unit')]) : null), + fact('status', first(vehicle, 'status'), { format: 'humanize' }), + ]; + }, + canOpen, + open: engineOpener(owner, 'vehicle-actions'), + }, + { + key: 'customer', + labelKey: 'resource.customer', + icon: 'user-tag', + modelNames: ['customer'], + aliases: ['facilitator-customer'], + polymorphicTypes: ['fleet-ops:customer', 'Fleetbase\\FleetOps\\Models\\Customer'], + permission: 'fleet-ops view contact', + title: (customer) => first(customer, 'name', 'public_id'), + identifier: (customer) => typeLabel(first(customer, 'customer_type', 'type')) ?? 'Customer', + image: (customer) => photo(customer, 'customer'), + selectDetails: (customer) => [first(customer, 'email'), first(customer, 'phone')], + facts: (customer) => [ + fact('type', typeLabel(first(customer, 'customer_type', 'type'))), + fact('email', first(customer, 'email')), + fact('phone', first(customer, 'phone')), + fact('address', first(customer, 'address', 'address_street')), + relatedFact('place', relation(owner, customer, 'place'), 'place', first(customer, 'place.address')), + ], + canOpen, + open: engineOpener(owner, 'customer-actions'), + }, + { + key: 'contact', + labelKey: 'resource.contact', + icon: 'address-book', + modelNames: ['contact'], + aliases: ['facilitator-contact', 'customer-contact'], + polymorphicTypes: ['fleet-ops:contact', 'Fleetbase\\FleetOps\\Models\\Contact'], + permission: 'fleet-ops view contact', + title: (contact) => first(contact, 'name', 'public_id'), + identifier: (contact) => first(contact, 'title') ?? typeLabel(first(contact, 'type')), + image: (contact) => photo(contact, 'contact'), + selectDetails: (contact) => [first(contact, 'email'), first(contact, 'phone')], + facts: (contact) => [ + fact('title', first(contact, 'title')), + fact('email', first(contact, 'email')), + fact('phone', first(contact, 'phone')), + relatedFact('place', relation(owner, contact, 'place'), 'place', first(contact, 'address', 'place.address')), + relatedFact('user', relation(owner, contact, 'user'), 'user', first(contact, 'user.name')), + ], + canOpen, + open: engineOpener(owner, 'contact-actions'), + }, + { + key: 'place', + labelKey: 'resource.place', + icon: 'location-dot', + modelNames: ['place'], + polymorphicTypes: ['fleet-ops:place', 'Fleetbase\\FleetOps\\Models\\Place'], + permission: 'fleet-ops view place', + title: (place) => first(place, 'displayName', 'name', 'address', 'street1', 'public_id'), + identifier: (place) => join([first(place, 'city'), first(place, 'country')], ', ') ?? first(place, 'public_id'), + image: () => icon('location-dot'), + selectDetails: (place) => [first(place, 'address', 'street1'), join([first(place, 'city'), first(place, 'country')], ', ')], + facts: (place) => [ + fact('address', first(place, 'address', 'street1')), + fact('phone', first(place, 'phone')), + fact('type', first(place, 'type'), { format: 'humanize' }), + relatedFact('vendor', relation(owner, place, 'vendor'), 'vendor', first(place, 'vendor_name')), + fact('coordinates', first(place, 'positionString') ?? (present(get(place, 'latitude')) ? `${get(place, 'latitude')}, ${get(place, 'longitude')}` : null)), + ], + canOpen, + open: engineOpener(owner, 'place-actions'), + }, + { + key: 'order', + labelKey: 'resource.order', + icon: 'box', + modelNames: ['order'], + polymorphicTypes: ['fleet-ops:order', 'Fleetbase\\FleetOps\\Models\\Order'], + permission: 'fleet-ops view order', + statusTones: { + created: 'text-gray-400', + preparing: 'text-yellow-500', + dispatched: 'text-yellow-500', + started: 'text-yellow-500', + in_progress: 'text-yellow-500', + enroute: 'text-yellow-500', + completed: 'text-green-500', + delivered: 'text-green-500', + canceled: 'text-red-500', + cancelled: 'text-red-500', + failed: 'text-red-500', + }, + title: (order) => first(order, 'tracking', 'tracking_number.tracking_number', 'public_id'), + identifier: (order) => first(order, 'public_id', 'internal_id'), + image: () => icon('box'), + status: (order) => first(order, 'status'), + selectDetails: (order) => [first(order, 'public_id'), first(order, 'status')], + facts: (order) => [ + fact('status', first(order, 'status'), { format: 'humanize' }), + relatedFact('customer', relation(owner, order, 'customer'), polymorphicType(order, 'customer', 'customer_type'), first(order, 'customer_name')), + relatedFact('driver', relation(owner, order, 'driver_assigned'), 'driver', first(order, 'driver_name')), + fact('route', join([first(order, 'pickupName', 'pickup_name'), first(order, 'dropoffName', 'dropoff_name')], ' → ')), + fact('scheduled', get(order, 'scheduled_at'), { format: 'date' }), + fact('order-config', first(order, 'order_config.name', 'type')), + ], + canOpen, + open: guardOrder(engineOpener(owner, 'order-actions')), + }, + { + key: 'vendor', + labelKey: 'resource.vendor', + icon: 'building', + modelNames: ['vendor'], + aliases: ['facilitator-vendor', 'customer-vendor'], + polymorphicTypes: ['fleet-ops:vendor', 'Fleetbase\\FleetOps\\Models\\Vendor'], + permission: 'fleet-ops view vendor', + title: (vendor) => first(vendor, 'name', 'public_id'), + identifier: (vendor) => first(vendor, 'prettyType', 'internal_id', 'business_id'), + image: (vendor) => photo(vendor, 'vendor', 'logo_url'), + status: (vendor) => first(vendor, 'status'), + selectDetails: (vendor) => [first(vendor, 'email'), first(vendor, 'phone')], + facts: (vendor) => [ + fact('type', first(vendor, 'prettyType', 'type'), { format: 'humanize' }), + fact('email', first(vendor, 'email')), + fact('phone', first(vendor, 'phone')), + fact('website', first(vendor, 'website_url')), + fact('address', first(vendor, 'address', 'place.address', 'address_street')), + fact('country', first(vendor, 'country')), + ], + canOpen, + open: engineOpener(owner, 'vendor-actions'), + }, + { + key: 'fleet', + labelKey: 'resource.fleet', + icon: 'layer-group', + modelNames: ['fleet'], + polymorphicTypes: ['fleet-ops:fleet', 'Fleetbase\\FleetOps\\Models\\Fleet'], + permission: 'fleet-ops view fleet', + title: (fleet) => first(fleet, 'name', 'public_id'), + identifier: (fleet) => first(fleet, 'task', 'public_id'), + image: (fleet) => photo(fleet, 'fleet'), + status: (fleet) => first(fleet, 'status'), + selectDetails: (fleet) => [first(fleet, 'task'), present(get(fleet, 'drivers_count')) ? `${get(fleet, 'drivers_count')} drivers` : null], + facts: (fleet) => [ + fact('task', first(fleet, 'task')), + relatedFact('service-area', relation(owner, fleet, 'service_area'), 'service-area', first(fleet, 'service_area.name')), + relatedFact('zone', relation(owner, fleet, 'zone'), 'zone', first(fleet, 'zone.name')), + relatedFact('vendor', relation(owner, fleet, 'vendor'), 'vendor', first(fleet, 'vendor.name')), + fact('drivers', present(get(fleet, 'drivers_count')) ? `${get(fleet, 'drivers_online_count') ?? 0} online of ${get(fleet, 'drivers_count')}` : null), + fact('vehicles', present(get(fleet, 'vehicles_count')) ? `${get(fleet, 'vehicles_online_count') ?? 0} online of ${get(fleet, 'vehicles_count')}` : null), + ], + canOpen, + open: engineOpener(owner, 'fleet-actions'), + }, + ].map((descriptor) => ({ + ...descriptor, + components: { identity: `cell/${descriptor.key}-identity`, ...(descriptor.components ?? {}) }, + })); +} + +/** + * An order row may only know its order by name (an identity stub) and a + * stub has nothing to open; a saved order must carry an id. + */ +export function guardOrder(open) { + return (order, ...rest) => { + if (!order || order.isIdentityStub || !first(order, 'id', 'uuid')) { + return false; + } + + return open(order, ...rest); + }; +} + +export default buildSharedResourceDescriptors; diff --git a/app/components/cell/contact-identity.js b/app/components/cell/contact-identity.js new file mode 100644 index 0000000..3a97c2a --- /dev/null +++ b/app/components/cell/contact-identity.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/cell/contact-identity'; diff --git a/app/components/cell/customer-identity.js b/app/components/cell/customer-identity.js new file mode 100644 index 0000000..7832618 --- /dev/null +++ b/app/components/cell/customer-identity.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/cell/customer-identity'; diff --git a/app/components/cell/driver-identity.js b/app/components/cell/driver-identity.js new file mode 100644 index 0000000..431d0ec --- /dev/null +++ b/app/components/cell/driver-identity.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/cell/driver-identity'; diff --git a/app/components/cell/fleet-identity.js b/app/components/cell/fleet-identity.js new file mode 100644 index 0000000..263b22b --- /dev/null +++ b/app/components/cell/fleet-identity.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/cell/fleet-identity'; diff --git a/app/components/cell/order-identity.js b/app/components/cell/order-identity.js new file mode 100644 index 0000000..4bb3613 --- /dev/null +++ b/app/components/cell/order-identity.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/cell/order-identity'; diff --git a/app/components/cell/place-identity.js b/app/components/cell/place-identity.js new file mode 100644 index 0000000..eb31bd1 --- /dev/null +++ b/app/components/cell/place-identity.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/cell/place-identity'; diff --git a/app/components/cell/vehicle-identity.js b/app/components/cell/vehicle-identity.js new file mode 100644 index 0000000..9e54d46 --- /dev/null +++ b/app/components/cell/vehicle-identity.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/cell/vehicle-identity'; diff --git a/app/components/cell/vendor-identity.js b/app/components/cell/vendor-identity.js new file mode 100644 index 0000000..9ef5d6c --- /dev/null +++ b/app/components/cell/vendor-identity.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/cell/vendor-identity'; diff --git a/app/components/contact/pill.js b/app/components/contact/pill.js new file mode 100644 index 0000000..c7a297a --- /dev/null +++ b/app/components/contact/pill.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/contact/pill'; diff --git a/app/components/contact/summary.js b/app/components/contact/summary.js new file mode 100644 index 0000000..7e7c800 --- /dev/null +++ b/app/components/contact/summary.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/contact/summary'; diff --git a/app/components/customer/pill.js b/app/components/customer/pill.js new file mode 100644 index 0000000..aea27bb --- /dev/null +++ b/app/components/customer/pill.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/customer/pill'; diff --git a/app/components/customer/summary.js b/app/components/customer/summary.js new file mode 100644 index 0000000..899a942 --- /dev/null +++ b/app/components/customer/summary.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/customer/summary'; diff --git a/app/components/driver/pill.js b/app/components/driver/pill.js new file mode 100644 index 0000000..3490441 --- /dev/null +++ b/app/components/driver/pill.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/driver/pill'; diff --git a/app/components/driver/summary.js b/app/components/driver/summary.js new file mode 100644 index 0000000..e96be83 --- /dev/null +++ b/app/components/driver/summary.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/driver/summary'; diff --git a/app/components/fleet/pill.js b/app/components/fleet/pill.js new file mode 100644 index 0000000..d849910 --- /dev/null +++ b/app/components/fleet/pill.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/fleet/pill'; diff --git a/app/components/fleet/summary.js b/app/components/fleet/summary.js new file mode 100644 index 0000000..e8a90c5 --- /dev/null +++ b/app/components/fleet/summary.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/fleet/summary'; diff --git a/app/components/order/pill.js b/app/components/order/pill.js new file mode 100644 index 0000000..f3ceb03 --- /dev/null +++ b/app/components/order/pill.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/order/pill'; diff --git a/app/components/order/summary.js b/app/components/order/summary.js new file mode 100644 index 0000000..24bf123 --- /dev/null +++ b/app/components/order/summary.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/order/summary'; diff --git a/app/components/place/pill.js b/app/components/place/pill.js new file mode 100644 index 0000000..2757d3a --- /dev/null +++ b/app/components/place/pill.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/place/pill'; diff --git a/app/components/place/summary.js b/app/components/place/summary.js new file mode 100644 index 0000000..3ee08c9 --- /dev/null +++ b/app/components/place/summary.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/place/summary'; diff --git a/app/components/select-option/contact.js b/app/components/select-option/contact.js new file mode 100644 index 0000000..fd9d008 --- /dev/null +++ b/app/components/select-option/contact.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/select-option/contact'; diff --git a/app/components/select-option/customer.js b/app/components/select-option/customer.js new file mode 100644 index 0000000..26fd3c3 --- /dev/null +++ b/app/components/select-option/customer.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/select-option/customer'; diff --git a/app/components/select-option/driver.js b/app/components/select-option/driver.js new file mode 100644 index 0000000..a08f3e0 --- /dev/null +++ b/app/components/select-option/driver.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/select-option/driver'; diff --git a/app/components/select-option/fleet.js b/app/components/select-option/fleet.js new file mode 100644 index 0000000..3e6eba6 --- /dev/null +++ b/app/components/select-option/fleet.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/select-option/fleet'; diff --git a/app/components/select-option/order.js b/app/components/select-option/order.js new file mode 100644 index 0000000..34dc932 --- /dev/null +++ b/app/components/select-option/order.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/select-option/order'; diff --git a/app/components/select-option/place.js b/app/components/select-option/place.js new file mode 100644 index 0000000..d78d889 --- /dev/null +++ b/app/components/select-option/place.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/select-option/place'; diff --git a/app/components/select-option/vehicle.js b/app/components/select-option/vehicle.js new file mode 100644 index 0000000..3c49271 --- /dev/null +++ b/app/components/select-option/vehicle.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/select-option/vehicle'; diff --git a/app/components/select-option/vendor.js b/app/components/select-option/vendor.js new file mode 100644 index 0000000..da45f9c --- /dev/null +++ b/app/components/select-option/vendor.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/select-option/vendor'; diff --git a/app/components/vehicle/pill.js b/app/components/vehicle/pill.js new file mode 100644 index 0000000..e1f78a1 --- /dev/null +++ b/app/components/vehicle/pill.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/vehicle/pill'; diff --git a/app/components/vehicle/summary.js b/app/components/vehicle/summary.js new file mode 100644 index 0000000..5dae0f5 --- /dev/null +++ b/app/components/vehicle/summary.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/vehicle/summary'; diff --git a/app/components/vendor/pill.js b/app/components/vendor/pill.js new file mode 100644 index 0000000..fa810e5 --- /dev/null +++ b/app/components/vendor/pill.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/vendor/pill'; diff --git a/app/components/vendor/summary.js b/app/components/vendor/summary.js new file mode 100644 index 0000000..b9f4eee --- /dev/null +++ b/app/components/vendor/summary.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/fleetops-data/components/vendor/summary'; diff --git a/app/instance-initializers/register-shared-resource-descriptors.js b/app/instance-initializers/register-shared-resource-descriptors.js new file mode 100644 index 0000000..3a54173 --- /dev/null +++ b/app/instance-initializers/register-shared-resource-descriptors.js @@ -0,0 +1 @@ +export { default, initialize } from '@fleetbase/fleetops-data/instance-initializers/register-shared-resource-descriptors'; diff --git a/package.json b/package.json index 527592d..365edaf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@fleetbase/fleetops-data", "version": "0.2.3", - "description": "Fleetbase Fleet-Ops based models, serializers, transforms, adapters and GeoJson utility functions.", + "description": "Fleetbase Fleet-Ops based models, serializers, transforms, adapters, GeoJson utilities and the shared resource identity descriptors.", "keywords": [ "fleetbase-data", "fleetops-data", diff --git a/tests/integration/components/resource-wrappers-test.js b/tests/integration/components/resource-wrappers-test.js new file mode 100644 index 0000000..d83997a --- /dev/null +++ b/tests/integration/components/resource-wrappers-test.js @@ -0,0 +1,58 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'dummy/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { helper } from '@ember/component/helper'; +import { SHARED_RESOURCE_KEYS } from '@fleetbase/fleetops-data/utils/resource-descriptors'; + +/** + * Each shared resource ships a pill, a summary, an identity cell and a + * select option that only name the resource type and hand everything else + * to the generic components the UI package provides. The UI package is not + * part of this addon, so those generics are stubbed here with templates + * that echo what they were given. + */ +module('Integration | Component | resource wrappers', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.owner.register( + 'helper:or', + helper(([a, b, c]) => a ?? b ?? c) + ); + this.owner.register('template:components/resource/pill', hbs`{{@resource.name}}`); + this.owner.register('template:components/resource/summary', hbs`{{@resource.name}}`); + this.owner.register('template:components/resource/select-option', hbs`{{@option.name}}`); + this.owner.register('template:components/table/cell/identity', hbs`{{@row.name}}`); + }); + + for (const key of SHARED_RESOURCE_KEYS) { + test(`${key} wrappers resolve and pass the ${key} resource type through`, async function (assert) { + this.set('record', { name: `A ${key}` }); + this.set('pill', `${key}/pill`); + this.set('summary', `${key}/summary`); + this.set('cell', `cell/${key}-identity`); + this.set('option', `select-option/${key}`); + + await render(hbs` + {{component this.pill resource=this.record}} + {{component this.summary resource=this.record}} + {{component this.cell row=this.record}} + {{component this.option option=this.record}} + `); + + assert.dom('[data-test-pill]').hasAttribute('data-type', key).hasText(`A ${key}`); + assert.dom('[data-test-summary]').hasAttribute('data-type', key).hasText(`A ${key}`); + assert.dom('[data-test-cell]').hasAttribute('data-type', key).hasText(`A ${key}`); + assert.dom('[data-test-option]').hasAttribute('data-type', key).hasText(`A ${key}`); + }); + } + + test('a pill still accepts the resource under its own name, as it always did', async function (assert) { + this.set('driver', { name: 'Ada Driver' }); + + await render(hbs``); + + assert.dom('[data-test-pill]').hasAttribute('data-type', 'driver').hasText('Ada Driver'); + }); +}); diff --git a/tests/unit/instance-initializers/register-shared-resource-descriptors-test.js b/tests/unit/instance-initializers/register-shared-resource-descriptors-test.js new file mode 100644 index 0000000..4b0f4d4 --- /dev/null +++ b/tests/unit/instance-initializers/register-shared-resource-descriptors-test.js @@ -0,0 +1,98 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'dummy/tests/helpers'; +import { initialize } from '@fleetbase/fleetops-data/instance-initializers/register-shared-resource-descriptors'; +import { SHARED_RESOURCE_KEYS } from '@fleetbase/fleetops-data/utils/resource-descriptors'; + +/** A stand-in for ember-ui's resource-registry service that remembers what it holds. */ +function fakeRegistry(existingKeys = []) { + const held = new Map(existingKeys.map((key) => [key, { key, open: 'kept' }])); + + return { + held, + registerDescriptors(list) { + list.forEach((descriptor) => held.set(descriptor.key, descriptor)); + }, + getDescriptor(key) { + return held.get(key) ?? null; + }, + }; +} + +function ownerWith(registry) { + return { lookup: (name) => (name === 'service:resource-registry' ? registry : null) }; +} + +module('Unit | Instance Initializer | register-shared-resource-descriptors', function (hooks) { + setupTest(hooks); + + test('it registers every shared descriptor on an empty registry', function (assert) { + const registry = fakeRegistry(); + + initialize(ownerWith(registry)); + + assert.deepEqual([...registry.held.keys()], SHARED_RESOURCE_KEYS); + assert.strictEqual(typeof registry.held.get('driver').open, 'function'); + }); + + test('it leaves descriptors another package already registered alone', function (assert) { + const registry = fakeRegistry(['driver', 'order']); + + initialize(ownerWith(registry)); + + assert.strictEqual(registry.held.get('driver').open, 'kept', 'the FleetOps driver descriptor survives'); + assert.strictEqual(registry.held.get('order').open, 'kept', 'the FleetOps order descriptor survives'); + assert.strictEqual(registry.held.size, SHARED_RESOURCE_KEYS.length, 'the missing ones were filled in'); + }); + + test('running twice registers nothing the second time', function (assert) { + const registry = fakeRegistry(); + initialize(ownerWith(registry)); + const first = registry.held.get('vehicle'); + + initialize(ownerWith(registry)); + + assert.strictEqual(registry.held.get('vehicle'), first, 'the same descriptor object remains'); + }); + + test('a registry that only lists its descriptors is read through that list', function (assert) { + const held = [{ key: 'place' }]; + const registry = { + descriptors: held, + registerDescriptors(list) { + held.push(...list); + }, + }; + + initialize(ownerWith(registry)); + + assert.strictEqual(held.filter((descriptor) => descriptor.key === 'place').length, 1, 'place was kept'); + assert.strictEqual(held.length, SHARED_RESOURCE_KEYS.length); + }); + + test('a registry whose lookup throws is treated as empty', function (assert) { + const registered = []; + const registry = { + registerDescriptors(list) { + registered.push(...list); + }, + getDescriptor() { + throw new Error('index not built'); + }, + }; + + initialize(ownerWith(registry)); + + assert.strictEqual(registered.length, SHARED_RESOURCE_KEYS.length); + }); + + test('it does nothing on a host without the registry service', function (assert) { + initialize({ lookup: () => null }); + initialize({ + lookup() { + throw new Error('no such service'); + }, + }); + + assert.ok(true, 'nothing thrown'); + }); +}); diff --git a/tests/unit/utils/placeholder-images-test.js b/tests/unit/utils/placeholder-images-test.js new file mode 100644 index 0000000..d555cfe --- /dev/null +++ b/tests/unit/utils/placeholder-images-test.js @@ -0,0 +1,63 @@ +import { module, test } from 'qunit'; +import ENV from 'dummy/config/environment'; +import getPlaceholderImage, { PLACEHOLDER_IMAGES, isDefaultImage, resolveResourceImage } from '@fleetbase/fleetops-data/utils/placeholder-images'; + +const INLINE = 'data:image/svg+xml;base64,PHN2Zy8+'; + +module('Unit | Utility | placeholder-images', function (hooks) { + let defaultValues; + + hooks.beforeEach(function () { + defaultValues = ENV.defaultValues; + ENV.defaultValues = { ...defaultValues }; + }); + + hooks.afterEach(function () { + ENV.defaultValues = defaultValues; + }); + + test('it ships one inline SVG silhouette per resource type', function (assert) { + for (const type of ['trailer', 'vehicle', 'driver', 'fleet', 'vendor', 'contact', 'customer']) { + assert.ok(PLACEHOLDER_IMAGES[type].startsWith('data:image/svg+xml;base64,'), `${type} placeholder is an inline data URI`); + assert.ok(atob(PLACEHOLDER_IMAGES[type].split(',')[1]).includes(' wins'); + + delete ENV.defaultValues.placeholders; + ENV.defaultValues.vehicleImage = INLINE; + assert.strictEqual(getPlaceholderImage('vehicle'), INLINE, 'an inline defaultValues.Image wins'); + + assert.strictEqual(getPlaceholderImage('vendor'), PLACEHOLDER_IMAGES.vendor, 'a hosted defaultValues.Image is ignored'); + + delete ENV.defaultValues; + assert.strictEqual(getPlaceholderImage('driver'), PLACEHOLDER_IMAGES.driver, 'no defaultValues at all is fine'); + }); + + test('it treats blank photos, the legacy hosted defaults and the configured defaults as "no photo"', function (assert) { + assert.true(isDefaultImage(undefined)); + assert.true(isDefaultImage('')); + assert.true(isDefaultImage(42)); + assert.true(isDefaultImage('https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/no-avatar.png')); + assert.true(isDefaultImage('https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/vehicle-placeholder.png')); + assert.true(isDefaultImage('https://flb-assets.s3.ap-southeast-1.amazonaws.com/static/image-file-icon.png')); + assert.true(isDefaultImage(ENV.defaultValues.driverImage), 'the console default is not a real photo'); + assert.false(isDefaultImage('https://cdn.example/uploads/truck-42.jpg')); + + delete ENV.defaultValues; + assert.false(isDefaultImage('https://cdn.example/uploads/truck-42.jpg'), 'no defaultValues at all is fine'); + }); + + test('it resolves a record image to its own photo or the typed placeholder', function (assert) { + assert.strictEqual(resolveResourceImage('https://cdn.example/uploads/truck-42.jpg', 'vehicle'), 'https://cdn.example/uploads/truck-42.jpg'); + assert.strictEqual(resolveResourceImage('https://s3.ap-southeast-1.amazonaws.com/flb-assets/static/no-avatar.png', 'driver'), PLACEHOLDER_IMAGES.driver); + assert.strictEqual(resolveResourceImage(null, 'customer'), PLACEHOLDER_IMAGES.customer); + }); +}); diff --git a/tests/unit/utils/resource-descriptors/helpers-test.js b/tests/unit/utils/resource-descriptors/helpers-test.js new file mode 100644 index 0000000..b633d50 --- /dev/null +++ b/tests/unit/utils/resource-descriptors/helpers-test.js @@ -0,0 +1,207 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'dummy/tests/helpers'; +import { + present, + first, + lookupService, + relation, + photo, + icon, + badge, + badges, + fact, + relatedFact, + dateLabel, + typeLabel, + money, + join, + polymorphicType, + extensionManager, + engineInstalled, + engineOpener, + FLEETOPS_ENGINE, +} from '@fleetbase/fleetops-data/utils/resource-descriptors/helpers'; +import { PLACEHOLDER_IMAGES } from '@fleetbase/fleetops-data/utils/placeholder-images'; + +module('Unit | Utility | resource-descriptors/helpers', function (hooks) { + setupTest(hooks); + + test('present and first read attributes by path and skip blanks', function (assert) { + assert.false(present(undefined)); + assert.false(present(null)); + assert.false(present(' ')); + assert.true(present(0)); + assert.true(present('x')); + + assert.strictEqual(first(null, 'name'), null, 'no record'); + assert.strictEqual(first({ name: '', nested: { label: 'Deep' } }, 'name', 'nested.label'), 'Deep'); + assert.strictEqual(first({ name: '' }, 'name', 'missing'), null); + }); + + test('lookupService returns null when the owner cannot provide the service', function (assert) { + assert.strictEqual(lookupService(null, 'anything'), null); + assert.strictEqual(lookupService({ lookup: () => null }, 'anything'), null); + assert.strictEqual( + lookupService( + { + lookup() { + throw new Error('nope'); + }, + }, + 'anything' + ), + null + ); + + const service = {}; + assert.strictEqual(lookupService({ lookup: () => service }, 'anything'), service); + }); + + test('relation goes through the resource registry when there is one', function (assert) { + const vehicle = { id: 'vehicle_1' }; + const registry = { relationValue: (record, name) => (name === 'vehicle' ? vehicle : null) }; + const owner = { lookup: (name) => (name === 'service:resource-registry' ? registry : null) }; + + assert.strictEqual(relation(owner, { vehicle: 'ignored' }, 'vehicle'), vehicle); + }); + + test('relation falls back to the attribute, unwrapping a promise proxy, without a registry', function (assert) { + const owner = { lookup: () => null }; + const vehicle = { id: 'vehicle_1' }; + + assert.strictEqual(relation(owner, null, 'vehicle'), null, 'no record'); + assert.strictEqual(relation(owner, {}, 'vehicle'), null, 'no relation'); + assert.strictEqual(relation(owner, { vehicle }, 'vehicle'), vehicle, 'a plain value'); + assert.strictEqual(relation(owner, { vehicle: { then() {}, content: vehicle } }, 'vehicle'), vehicle, 'a resolved proxy'); + assert.strictEqual(relation(owner, { vehicle: { then() {} } }, 'vehicle'), null, 'an unresolved proxy'); + }); + + test('photo pairs the record photo with the typed placeholder and shape', function (assert) { + assert.deepEqual(photo({ photo_url: 'https://cdn.test/a.png' }, 'driver'), { url: 'https://cdn.test/a.png', fallback: PLACEHOLDER_IMAGES.driver, shape: 'round' }); + assert.deepEqual(photo({}, 'vehicle'), { url: PLACEHOLDER_IMAGES.vehicle, fallback: PLACEHOLDER_IMAGES.vehicle, shape: 'square' }); + assert.strictEqual(photo({}, 'trailer').shape, 'square'); + assert.strictEqual(photo({ logo_url: 'https://cdn.test/logo.png' }, 'vendor', 'logo_url').url, 'https://cdn.test/logo.png'); + }); + + test('icon, badge, badges, fact and relatedFact build the descriptor fragments', function (assert) { + assert.deepEqual(icon('box'), { icon: 'box' }); + assert.deepEqual(icon('box', 'tile'), { icon: 'box', iconClass: 'tile' }); + + assert.strictEqual(badge('plate', 'id-card', ''), null, 'a blank label is no badge'); + assert.deepEqual(badge('plate', 'id-card', 'ABC', { relatedId: 'v1' }), { key: 'plate', icon: 'id-card', label: 'ABC', relatedId: 'v1' }); + assert.deepEqual(badges(null, { key: 'a' }, undefined, { key: 'b' }), [{ key: 'a' }, { key: 'b' }]); + + assert.deepEqual(fact('phone', '123'), { labelKey: 'resource-summary.facts.phone', value: '123' }); + assert.deepEqual(fact('status', 'active', { format: 'humanize' }), { labelKey: 'resource-summary.facts.status', value: 'active', format: 'humanize' }); + assert.deepEqual(relatedFact('vehicle', undefined, 'vehicle'), { labelKey: 'resource-summary.facts.vehicle', related: null, relatedType: 'vehicle', value: null }); + const vehicle = {}; + assert.deepEqual(relatedFact('vehicle', vehicle, 'vehicle', 'Truck'), { labelKey: 'resource-summary.facts.vehicle', related: vehicle, relatedType: 'vehicle', value: 'Truck' }); + }); + + test('dateLabel formats dates and strings and refuses anything else', function (assert) { + assert.strictEqual(dateLabel(null), null); + assert.strictEqual(dateLabel('not a date'), null); + assert.strictEqual(dateLabel(new Date(2026, 0, 2, 3, 4)), '02 Jan 2026, 03:04'); + assert.strictEqual(dateLabel('2026-01-02T00:00:00', 'dd MMM yyyy'), '02 Jan 2026'); + }); + + test('typeLabel reads a polymorphic type the way a person does', function (assert) { + assert.strictEqual(typeLabel(''), null); + assert.strictEqual(typeLabel('customer-fleet-ops:contact'), 'Customer'); + assert.strictEqual(typeLabel('facilitator_vendor'), 'Facilitator vendor'); + assert.strictEqual(typeLabel(':contact'), null, 'nothing readable before the colon'); + }); + + test('money and join leave out what is missing', function (assert) { + assert.strictEqual(money(null, 'USD'), null); + assert.strictEqual(money(10), '10'); + assert.strictEqual(money(10, 'USD'), '10 USD'); + assert.strictEqual(join([null, '', 'a', 'b']), 'a b'); + assert.strictEqual(join(['a', 'b'], ', '), 'a, b'); + assert.strictEqual(join([null]), null); + }); + + test('polymorphicType reads the related model name, its proxy, or the type attribute', function (assert) { + class Customer { + static modelName = 'contact'; + } + + assert.strictEqual(polymorphicType(null, 'customer', 'customer_type'), null); + assert.strictEqual(polymorphicType({ customer: new Customer() }, 'customer', 'customer_type'), 'contact'); + assert.strictEqual(polymorphicType({ customer: { content: new Customer() } }, 'customer', 'customer_type'), 'contact'); + assert.strictEqual(polymorphicType({ customer: null, customer_type: 'vendor' }, 'customer', 'customer_type'), 'vendor'); + assert.strictEqual(polymorphicType({ customer: null }, 'customer'), null); + }); + + module('reaching the FleetOps engine', function () { + test('extensionManager is found as its own service or through universe', function (assert) { + const manager = {}; + + assert.strictEqual(extensionManager({ lookup: () => null }), null); + assert.strictEqual(extensionManager({ lookup: (name) => (name === 'service:universe/extension-manager' ? manager : null) }), manager); + assert.strictEqual(extensionManager({ lookup: (name) => (name === 'service:universe' ? { extensionManager: manager } : null) }), manager); + }); + + test('engineInstalled is false without a manager, when not installed, or when the check throws', function (assert) { + assert.false(engineInstalled({ lookup: () => null })); + assert.false(engineInstalled({ lookup: () => ({ isInstalled: () => false }) })); + assert.true(engineInstalled({ lookup: () => ({ isInstalled: (name) => name === FLEETOPS_ENGINE }) })); + assert.true(engineInstalled({ lookup: () => ({ isInstalled: (name) => name === '@other/engine' }) }, '@other/engine')); + assert.false( + engineInstalled({ + lookup: () => ({ + isInstalled() { + throw new Error('boom'); + }, + }), + }) + ); + }); + + function ownerWithEngine(services, overrides = {}) { + const engine = { lookup: (name) => services[name.replace('service:', '')] ?? null }; + const manager = { ensureEngineLoaded: async () => engine, ...overrides }; + + return { lookup: (name) => (name === 'service:universe/extension-manager' ? manager : null) }; + } + + test('engineOpener prefers the panel, then the transition, and reports what it could not do', async function (assert) { + const record = { id: 'r1' }; + const seen = []; + const panelService = { panel: { view: (r) => seen.push(['panel', r]) }, transition: { view: (r) => seen.push(['transition', r]) } }; + const transitionService = { transition: { view: (r) => seen.push(['transition-only', r]) } }; + const bareService = {}; + + assert.false(await engineOpener({ lookup: () => null }, 'driver-actions')(record), 'no manager'); + assert.false(await engineOpener(ownerWithEngine({ 'driver-actions': panelService }), 'driver-actions')(null), 'no record'); + assert.false(await engineOpener(ownerWithEngine({}), 'driver-actions')(record), 'no service'); + assert.false(await engineOpener(ownerWithEngine({ 'driver-actions': bareService }), 'driver-actions')(record), 'no view'); + assert.false(await engineOpener(ownerWithEngine({ 'driver-actions': panelService }, { ensureEngineLoaded: async () => null }), 'driver-actions')(record), 'no engine'); + assert.false( + await engineOpener( + ownerWithEngine( + { 'driver-actions': panelService }, + { + ensureEngineLoaded: async () => { + throw new Error('offline'); + }, + } + ), + 'driver-actions' + )(record), + 'the load failed' + ); + + assert.true(await engineOpener(ownerWithEngine({ 'driver-actions': panelService }), 'driver-actions')(record)); + assert.true(await engineOpener(ownerWithEngine({ 'driver-actions': transitionService }), 'driver-actions')(record), 'no panel means the transition'); + assert.true(await engineOpener(ownerWithEngine({ 'driver-actions': panelService }), 'driver-actions', { mode: 'transition' })(record)); + assert.false(await engineOpener(ownerWithEngine({ 'driver-actions': panelService }), 'driver-actions', { mode: 'modal' })(record), 'an unknown mode opens nothing'); + + assert.deepEqual(seen, [ + ['panel', record], + ['transition-only', record], + ['transition', record], + ]); + }); + }); +}); diff --git a/tests/unit/utils/resource-descriptors/shared-test.js b/tests/unit/utils/resource-descriptors/shared-test.js new file mode 100644 index 0000000..df9f01c --- /dev/null +++ b/tests/unit/utils/resource-descriptors/shared-test.js @@ -0,0 +1,270 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'dummy/tests/helpers'; +import { buildSharedResourceDescriptors, SHARED_RESOURCE_KEYS, guardOrder } from '@fleetbase/fleetops-data/utils/resource-descriptors'; + +const UUID = '0b2d9a6e-5e6b-4d6c-9a1f-3c8e1f2a4b5d'; + +/** Every descriptor field that takes a record, called with the given record. */ +function readAll(descriptor, record) { + const values = {}; + + for (const field of ['title', 'identifier', 'image', 'online', 'status', 'badges', 'selectDetails', 'facts']) { + if (typeof descriptor[field] === 'function') { + values[field] = descriptor[field](record); + } + } + + return values; +} + +function byKey(owner) { + return Object.fromEntries(buildSharedResourceDescriptors(owner).map((descriptor) => [descriptor.key, descriptor])); +} + +function factValue(facts, label) { + return facts.find((fact) => fact.labelKey === `resource-summary.facts.${label}`); +} + +module('Unit | Utility | resource-descriptors/shared', function (hooks) { + setupTest(hooks); + + test('there is one descriptor per shared resource, each naming its identity cell wrapper', function (assert) { + const descriptors = buildSharedResourceDescriptors(this.owner); + + assert.deepEqual( + descriptors.map((descriptor) => descriptor.key), + SHARED_RESOURCE_KEYS, + 'driver, vehicle, customer, contact, place, order, vendor and fleet' + ); + + for (const descriptor of descriptors) { + assert.strictEqual(descriptor.components.identity, `cell/${descriptor.key}-identity`, `${descriptor.key} identity cell`); + assert.ok(descriptor.modelNames.includes(descriptor.key), `${descriptor.key} names its model`); + assert.ok( + descriptor.polymorphicTypes.some((type) => type.startsWith('fleet-ops:')), + `${descriptor.key} names its fleet-ops type` + ); + assert.strictEqual(typeof descriptor.open, 'function', `${descriptor.key} can be opened`); + assert.strictEqual(typeof descriptor.canOpen, 'function', `${descriptor.key} guards opening on the engine`); + } + }); + + test('every field tolerates an empty record and a sparse one', function (assert) { + for (const descriptor of buildSharedResourceDescriptors(this.owner)) { + for (const record of [{}, { public_id: `${descriptor.key}_1` }]) { + let values; + + try { + values = readAll(descriptor, record); + } catch (error) { + assert.ok(false, `${descriptor.key} threw on ${JSON.stringify(record)}: ${error.message}`); + continue; + } + + assert.ok(Array.isArray(values.facts), `${descriptor.key} facts are a list`); + assert.ok(Array.isArray(values.selectDetails), `${descriptor.key} select details are a list`); + assert.ok(values.image && typeof values.image === 'object', `${descriptor.key} image is an object`); + } + } + }); + + test('identifiers never surface a UUID', function (assert) { + for (const descriptor of buildSharedResourceDescriptors(this.owner)) { + const record = { id: UUID, uuid: UUID, public_id: 'thing_1', name: 'Named', plate_number: null, driver_uuid: UUID, vehicle_uuid: UUID }; + const identifier = descriptor.identifier?.(record); + + assert.notStrictEqual(identifier, UUID, `${descriptor.key} identifier is not the uuid`); + } + }); + + test('a driver renders its name, phone, photo placeholder and online dot, and its vehicle as a badge', function (assert) { + const { driver } = byKey(this.owner); + const record = { + name: 'Ada Driver', + phone: '+15551234567', + online: true, + status: 'available', + vehicle_name: 'Truck 10', + vehicle_uuid: 'vehicle_1', + drivers_license_number: 'DL-1', + license_expiry: '2027-03-04T00:00:00', + }; + const values = readAll(driver, record); + + assert.strictEqual(values.title, 'Ada Driver'); + assert.strictEqual(values.identifier, '+15551234567'); + assert.true(values.online); + assert.strictEqual(values.status, 'available'); + assert.ok(values.image.url.startsWith('data:image/svg+xml'), 'no photo means the styled placeholder'); + assert.strictEqual(values.image.shape, 'round'); + assert.deepEqual( + values.badges.map((badge) => [badge.key, badge.label, badge.relatedId]), + [['vehicle', 'Truck 10', 'vehicle_1']] + ); + assert.strictEqual(factValue(values.facts, 'licence').value, 'DL-1 · expires 04 Mar 2027'); + assert.strictEqual(factValue(values.facts, 'vehicle').value, 'Truck 10'); + assert.strictEqual(driver.online({ online: 'yes' }), undefined, 'a non-boolean online flag is no dot'); + }); + + test('a driver with a loaded vehicle names it from the relation', function (assert) { + const { driver } = byKey(this.owner); + const vehicle = { id: 'vehicle_2', displayName: 'Van 2' }; + const values = readAll(driver, { name: 'Ada', vehicle }); + + assert.deepEqual( + values.badges.map((badge) => [badge.key, badge.label, badge.relatedId]), + [['vehicle', 'Van 2', 'vehicle_2']] + ); + assert.strictEqual(factValue(values.facts, 'vehicle').related, vehicle); + assert.strictEqual(factValue(values.facts, 'licence').value, null, 'no licence, no label'); + }); + + test('a vehicle is square, titles itself from year make model when it has no name, and lists its driver and trailers', function (assert) { + const { vehicle } = byKey(this.owner); + + assert.strictEqual(vehicle.image({ photo_url: 'https://cdn.test/truck.png' }).shape, 'square'); + assert.strictEqual(vehicle.image({ photo_url: 'https://cdn.test/truck.png' }).url, 'https://cdn.test/truck.png'); + assert.strictEqual(vehicle.title({ year: 2020, make: 'Ford', model: 'Transit' }), '2020 Ford Transit'); + assert.strictEqual(vehicle.title({ displayName: 'Truck 1', year: 2020 }), 'Truck 1'); + + const driver = { id: 'driver_1', name: 'Ada' }; + const loaded = readAll(vehicle, { plate_number: 'ABC-1', driver, trailers: [{ displayName: 'Box 1' }, { displayName: 'Box 2' }], odometer: 1200, odometer_unit: 'km' }); + assert.deepEqual( + loaded.badges.map((badge) => [badge.key, badge.label]), + [ + ['plate', 'ABC-1'], + ['driver', 'Ada'], + ] + ); + assert.strictEqual(factValue(loaded.facts, 'trailer').value, 'Box 1'); + assert.strictEqual(factValue(loaded.facts, 'trailer').suffix, ' (+1)'); + assert.strictEqual(factValue(loaded.facts, 'odometer').value, '1200 km'); + + const emberArray = readAll(vehicle, { trailers: { length: 1, objectAt: (index) => (index === 0 ? { displayName: 'Box 1' } : null) } }); + assert.strictEqual(factValue(emberArray.facts, 'trailer').value, 'Box 1', 'a many-array is read through objectAt'); + assert.strictEqual(factValue(emberArray.facts, 'trailer').suffix, ''); + + const arrayLike = readAll(vehicle, { trailers: { 0: { displayName: 'Box 1' }, length: 1 } }); + assert.strictEqual(factValue(arrayLike.facts, 'trailer').value, 'Box 1', 'a plain list is read by index'); + + const bare = readAll(vehicle, { driver_name: 'Bob', driver_uuid: 'driver_9' }); + assert.deepEqual( + bare.badges.map((badge) => [badge.key, badge.label, badge.relatedId]), + [['driver', 'Bob', 'driver_9']] + ); + assert.strictEqual(factValue(bare.facts, 'trailer').value, null); + assert.strictEqual(factValue(bare.facts, 'odometer').value, null); + }); + + test('a place is an icon with its city and country, and a coordinate fact', function (assert) { + const { place } = byKey(this.owner); + + assert.deepEqual(place.image({}), { icon: 'location-dot' }); + assert.strictEqual(place.identifier({ city: 'Singapore', country: 'SG' }), 'Singapore, SG'); + assert.strictEqual(place.identifier({ public_id: 'place_1' }), 'place_1'); + assert.strictEqual(factValue(place.facts({ positionString: '1.3, 103.8' }), 'coordinates').value, '1.3, 103.8'); + assert.strictEqual(factValue(place.facts({ latitude: 1.3, longitude: 103.8 }), 'coordinates').value, '1.3, 103.8'); + assert.strictEqual(factValue(place.facts({}), 'coordinates').value, null); + }); + + test('an order reads its tracking number and names its customer by its polymorphic type', function (assert) { + const { order } = byKey(this.owner); + + assert.strictEqual(order.title({ tracking: 'TRK-1', public_id: 'order_1' }), 'TRK-1'); + assert.strictEqual(order.identifier({ public_id: 'order_1' }), 'order_1'); + const facts = order.facts({ customer_type: 'contact', customer_name: 'Ada', pickupName: 'A', dropoffName: 'B' }); + assert.strictEqual(factValue(facts, 'customer').relatedType, 'contact'); + assert.strictEqual(factValue(facts, 'customer').value, 'Ada'); + assert.strictEqual(factValue(facts, 'route').value, 'A → B'); + }); + + test('customers, contacts, vendors and fleets read their identifiers', function (assert) { + const { customer, contact, vendor, fleet } = byKey(this.owner); + + assert.strictEqual(customer.identifier({ customer_type: 'customer-fleet-ops:contact' }), 'Customer'); + assert.strictEqual(customer.identifier({}), 'Customer'); + assert.strictEqual(contact.identifier({ title: 'Manager', type: 'facilitator' }), 'Manager'); + assert.strictEqual(contact.identifier({ type: 'facilitator' }), 'Facilitator'); + assert.strictEqual(vendor.image({ logo_url: 'https://cdn.test/logo.png' }).url, 'https://cdn.test/logo.png'); + assert.deepEqual(fleet.selectDetails({ task: 'Deliveries', drivers_count: 3 }), ['Deliveries', '3 drivers']); + assert.deepEqual(fleet.selectDetails({}), [null, null]); + const facts = fleet.facts({ drivers_count: 3, vehicles_count: 2, vehicles_online_count: 1 }); + assert.strictEqual(factValue(facts, 'drivers').value, '0 online of 3'); + assert.strictEqual(factValue(facts, 'vehicles').value, '1 online of 2'); + assert.strictEqual(factValue(fleet.facts({ vehicles_count: 2 }), 'vehicles').value, '0 online of 2'); + }); + + module('opening through the FleetOps engine', function () { + function fakeManager(overrides = {}) { + const service = { + panel: { + view(record) { + service.viewed = record; + }, + }, + }; + const engine = { lookup: (name) => (name === 'service:driver-actions' ? service : null) }; + const manager = { + installed: true, + loads: 0, + isInstalled() { + return this.installed; + }, + async ensureEngineLoaded(name) { + this.loads++; + this.loadedName = name; + return engine; + }, + ...overrides, + }; + + return { manager, service }; + } + + test('canOpen is true only when the FleetOps extension is installed', function (assert) { + const { manager } = fakeManager(); + this.owner.register('service:universe/extension-manager', manager, { instantiate: false }); + const [driver] = buildSharedResourceDescriptors(this.owner); + + assert.true(driver.canOpen({})); + + manager.installed = false; + assert.false(driver.canOpen({})); + }); + + test('canOpen is false on a host without the extension manager', function (assert) { + const [driver] = buildSharedResourceDescriptors(this.owner); + + assert.false(driver.canOpen({})); + }); + + test('open loads the engine on demand, then hands the record to its action service panel', async function (assert) { + const { manager, service } = fakeManager(); + this.owner.register('service:universe/extension-manager', manager, { instantiate: false }); + const [driver] = buildSharedResourceDescriptors(this.owner); + const record = { id: 'driver_1', name: 'Ada' }; + + assert.true(await driver.open(record)); + assert.strictEqual(manager.loads, 1, 'the engine was loaded'); + assert.strictEqual(manager.loadedName, '@fleetbase/fleetops-engine'); + assert.strictEqual(service.viewed, record, 'the panel shows the record'); + }); + + test('an order stub or an unsaved order never opens', async function (assert) { + let opened = 0; + const open = guardOrder(() => { + opened++; + return true; + }); + + assert.false(open(null)); + assert.false(open({ isIdentityStub: true, name: 'ORD-1' })); + assert.false(open({ public_id: 'order_1' })); + assert.true(open({ id: 'order_1' })); + assert.strictEqual(opened, 1); + + const { order } = byKey(this.owner); + assert.false(await order.open({ isIdentityStub: true }), 'the shared order descriptor is guarded'); + }); + }); +}); From 28b95795a345404925f320b57710ff2d3fddbf97 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 9 Oct 2026 17:19:50 +0800 Subject: [PATCH 04/10] feat(identity-stub): share the identity stub builder for relations a row only knows by name --- README.md | 2 +- addon/utils/identity-stub.js | 56 +++++++++++++++++++++++++ tests/unit/utils/identity-stub-test.js | 57 ++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 addon/utils/identity-stub.js create mode 100644 tests/unit/utils/identity-stub-test.js diff --git a/README.md b/README.md index 5e878ef..653d868 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ An instance initializer registers the descriptors with the `resource-registry` s Opening a resource loads the Fleet-Ops engine on demand and hands the record to its action service, so the engine bundle is only fetched when someone clicks. When Fleet-Ops itself boots it replaces these descriptors with its own, which open the same panels directly. -The helpers behind the descriptors and the styled placeholder images are exported from `@fleetbase/fleetops-data/utils/resource-descriptors` and `@fleetbase/fleetops-data/utils/placeholder-images` for other packages that build descriptors. +The helpers behind the descriptors and the styled placeholder images are exported from `@fleetbase/fleetops-data/utils/resource-descriptors` and `@fleetbase/fleetops-data/utils/placeholder-images` for other packages that build descriptors. `@fleetbase/fleetops-data/utils/identity-stub` builds a stand-in record for a relation a row only knows by name (`driver_name`, `pickupName`), so an identity cell can render it and load the real record on demand. ## Contributing diff --git a/addon/utils/identity-stub.js b/addon/utils/identity-stub.js new file mode 100644 index 0000000..ee67944 --- /dev/null +++ b/addon/utils/identity-stub.js @@ -0,0 +1,56 @@ +import { get } from '@ember/object'; + +const inflight = new WeakMap(); + +/** + * A stand-in for a related record a row only knows by name: enough for an + * identity cell, pill or summary to render (`name`, `resourceType`) and a + * `loadResource()` they call when the real record is needed. The stub never + * carries a UUID in an identifier field, so nothing renders one as a plate + * or an id, and concurrent loads of the same row share one request. + * + * `load` receives the row and returns the record (or a promise of it); with + * no `load`, `loadResource()` resolves to null and the stub stays static. + */ +export function buildIdentityStub(row, { type, nameKey = `${type}_name`, name, load, extra = {} } = {}) { + const label = name ?? (row ? get(row, nameKey) : null); + + if (!label) { + return null; + } + + return { + ...extra, + name: label, + display_name: label, + displayName: label, + resourceType: type, + isIdentityStub: true, + loadResource: async () => { + if (typeof load !== 'function') { + return null; + } + + let loads = inflight.get(row); + + if (!loads) { + loads = new Map(); + inflight.set(row, loads); + } + + if (!loads.has(type)) { + loads.set( + type, + Promise.resolve() + .then(() => load(row)) + .catch(() => null) + .finally(() => loads.delete(type)) + ); + } + + return loads.get(type); + }, + }; +} + +export default buildIdentityStub; diff --git a/tests/unit/utils/identity-stub-test.js b/tests/unit/utils/identity-stub-test.js new file mode 100644 index 0000000..b68843f --- /dev/null +++ b/tests/unit/utils/identity-stub-test.js @@ -0,0 +1,57 @@ +import { module, test } from 'qunit'; +import buildIdentityStub from '@fleetbase/fleetops-data/utils/identity-stub'; + +module('Unit | Utility | identity-stub', function () { + test('it names the stub from the row, an explicit name, or not at all', function (assert) { + const stub = buildIdentityStub({ driver_name: 'Ada' }, { type: 'driver' }); + + assert.strictEqual(stub.name, 'Ada'); + assert.strictEqual(stub.displayName, 'Ada'); + assert.strictEqual(stub.display_name, 'Ada'); + assert.strictEqual(stub.resourceType, 'driver'); + assert.true(stub.isIdentityStub); + + assert.strictEqual(buildIdentityStub({ pickupName: 'Depot' }, { type: 'place', nameKey: 'pickupName' }).name, 'Depot'); + assert.strictEqual(buildIdentityStub(null, { type: 'place', name: 'Depot', extra: { city: 'SG' } }).city, 'SG'); + assert.strictEqual(buildIdentityStub({}, { type: 'driver' }), null, 'a row without the name has no stub'); + assert.strictEqual(buildIdentityStub(null, { type: 'driver' }), null, 'no row, no stub'); + }); + + test('loadResource resolves to null without a loader', async function (assert) { + const stub = buildIdentityStub({ driver_name: 'Ada' }, { type: 'driver' }); + + assert.strictEqual(await stub.loadResource(), null); + }); + + test('concurrent loads of one row share a request, a failed load resolves to null, and a later load runs again', async function (assert) { + const row = { driver_name: 'Ada' }; + const record = { id: 'driver_1' }; + let calls = 0; + const stub = buildIdentityStub(row, { + type: 'driver', + load: () => { + calls++; + return calls === 2 ? Promise.reject(new Error('offline')) : record; + }, + }); + + const [first, second] = await Promise.all([stub.loadResource(), stub.loadResource()]); + assert.strictEqual(first, record); + assert.strictEqual(second, record); + assert.strictEqual(calls, 1, 'one request for both callers'); + + assert.strictEqual(await stub.loadResource(), null, 'the failed second load resolves to null'); + assert.strictEqual(calls, 2); + + assert.strictEqual(await stub.loadResource(), record, 'the third load runs afresh'); + assert.strictEqual(calls, 3); + }); + + test('two stubs of different types on one row load independently', async function (assert) { + const row = { driver_name: 'Ada', vehicle_name: 'Truck' }; + const driver = buildIdentityStub(row, { type: 'driver', load: () => 'D' }); + const vehicle = buildIdentityStub(row, { type: 'vehicle', load: () => 'V' }); + + assert.deepEqual(await Promise.all([driver.loadResource(), vehicle.loadResource()]), ['D', 'V']); + }); +}); From b0efe7d1430924fe7e71e6f02470bc449b1244e4 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 9 Oct 2026 17:38:30 +0800 Subject: [PATCH 05/10] test(identity-stub): cover a call without options --- tests/unit/utils/identity-stub-test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/utils/identity-stub-test.js b/tests/unit/utils/identity-stub-test.js index b68843f..ce5bdbb 100644 --- a/tests/unit/utils/identity-stub-test.js +++ b/tests/unit/utils/identity-stub-test.js @@ -15,6 +15,7 @@ module('Unit | Utility | identity-stub', function () { assert.strictEqual(buildIdentityStub(null, { type: 'place', name: 'Depot', extra: { city: 'SG' } }).city, 'SG'); assert.strictEqual(buildIdentityStub({}, { type: 'driver' }), null, 'a row without the name has no stub'); assert.strictEqual(buildIdentityStub(null, { type: 'driver' }), null, 'no row, no stub'); + assert.strictEqual(buildIdentityStub({ driver_name: 'Ada' }), null, 'no options means no type to name the stub after'); }); test('loadResource resolves to null without a loader', async function (assert) { From 22f94423dc332bfcf16132f40fdba78292b68cb3 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Fri, 9 Oct 2026 20:42:53 +0800 Subject: [PATCH 06/10] fix(descriptors): read a bare fleet-ops:contact type as Contact and survive a universe without an extension manager typeLabel('fleet-ops:contact') read as "Fleet ops"; the part after the colon is the type when nothing precedes the extension prefix. extensionManager() now guards the universe service's lazy manager lookup, which throws on a host that has none, so canOpen stays false instead of failing. --- addon/utils/resource-descriptors/helpers.js | 22 +- .../addon/adapters/application.js.html | 796 +++++ .../ember-core/addon/adapters/index.html | 116 + .../addon/contracts/base-contract.js.html | 412 +++ .../contracts/extension-component.js.html | 604 ++++ .../addon/contracts/hook-registry.js.html | 139 + .../ember-core/addon/contracts/hook.js.html | 715 +++++ .../ember-core/addon/contracts/index.html | 236 ++ .../addon/contracts/menu-item.js.html | 1834 +++++++++++ .../addon/contracts/menu-panel.js.html | 592 ++++ .../addon/contracts/template-helper.js.html | 367 +++ .../addon/contracts/universe-registry.js.html | 157 + .../ember-core/addon/contracts/widget.js.html | 910 ++++++ .../ember-core/addon/initializers/index.html | 131 + .../load-socketcluster-client.js.html | 127 + .../local-storage-adapter.js.html | 148 + .../addon/serializers/application.js.html | 331 ++ .../ember-core/addon/serializers/index.html | 116 + .../addon/services/current-user.js.html | 1447 +++++++++ .../ember-core/addon/services/events.js.html | 1435 +++++++++ .../ember-core/addon/services/fetch.js.html | 2236 +++++++++++++ .../ember-core/addon/services/index.html | 206 ++ .../addon/services/notifications.js.html | 184 ++ .../ember-core/addon/services/session.js.html | 1093 +++++++ .../addon/services/universe.js.html | 2848 +++++++++++++++++ .../services/universe/hook-service.js.html | 1099 +++++++ .../addon/services/universe/index.html | 176 + .../services/universe/menu-service.js.html | 1951 +++++++++++ .../universe/registry-service.js.html | 1924 +++++++++++ .../universe/resource-view-service.js.html | 1177 +++++++ .../services/universe/widget-service.js.html | 1201 +++++++ .../addon/services/url-search-params.js.html | 919 ++++++ .../ember-core/addon/transforms/array.js.html | 172 + .../ember-core/addon/transforms/index.html | 146 + .../addon/transforms/object.js.html | 118 + .../ember-core/addon/transforms/raw.js.html | 118 + .../ember-core/addon/utils/corslite.js.html | 385 +++ .../ember-core/addon/utils/download.js.html | 151 + .../addon/utils/get-mime-type.js.html | 154 + .../addon/utils/get-user-options.js.html | 148 + .../addon/utils/get-with-default.js.html | 103 + .../addon/utils/has-json-structure.js.html | 115 + .../ember-core/addon/utils/index.html | 356 +++ .../addon/utils/is-empty-object.js.html | 103 + .../addon/utils/is-iterable.js.html | 106 + .../addon/utils/is-latitude.js.html | 94 + .../addon/utils/is-longitude.js.html | 94 + .../ember-core/addon/utils/is-model.js.html | 103 + .../addon/utils/is-not-model.js.html | 100 + .../ember-core/addon/utils/is-object.js.html | 94 + .../addon/utils/is-valid-coordinates.js.html | 199 ++ .../addon/utils/lookup-user-ip.js.html | 817 +++++ .../utils/merge-registered-items.js.html | 676 ++++ .../utils/resource-view-registry.js.html | 466 +++ .../addon/utils/serialize/index.html | 131 + ...alize-polymorphic-type-within-hash.js.html | 166 + .../normalize-polymorphic-type.js.html | 124 + .../resource-descriptors/helpers-test.js | 18 +- 58 files changed, 30899 insertions(+), 7 deletions(-) create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/adapters/application.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/adapters/index.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/base-contract.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/extension-component.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/hook-registry.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/hook.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/index.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/menu-item.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/menu-panel.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/template-helper.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/universe-registry.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/widget.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers/index.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers/load-socketcluster-client.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers/local-storage-adapter.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/serializers/application.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/serializers/index.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/current-user.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/events.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/fetch.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/index.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/notifications.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/session.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/hook-service.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/index.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/menu-service.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/registry-service.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/resource-view-service.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/widget-service.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/url-search-params.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/array.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/index.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/object.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/raw.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/corslite.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/download.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-mime-type.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-user-options.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-with-default.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/has-json-structure.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/index.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-empty-object.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-iterable.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-latitude.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-longitude.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-model.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-not-model.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-object.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-valid-coordinates.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/lookup-user-ip.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/merge-registered-items.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/resource-view-registry.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize/index.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize/normalize-polymorphic-type-within-hash.js.html create mode 100644 console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize/normalize-polymorphic-type.js.html diff --git a/addon/utils/resource-descriptors/helpers.js b/addon/utils/resource-descriptors/helpers.js index 07e88f7..3948d4c 100644 --- a/addon/utils/resource-descriptors/helpers.js +++ b/addon/utils/resource-descriptors/helpers.js @@ -107,11 +107,10 @@ export function typeLabel(value) { return null; } - const head = String(value) - .split(':')[0] - .replace(/-fleet-ops$/, '') - .replace(/[-_]+/g, ' ') - .trim(); + const [prefix, ...rest] = String(value).split(':'); + // "customer-fleet-ops:contact" reads as "Customer"; a bare "fleet-ops:contact" has + // nothing before the extension name, so it reads as the type after the colon. + const head = (prefix.replace(/-?fleet-ops$/, '') || rest.join(':')).replace(/[-_]+/g, ' ').trim(); return head ? head.charAt(0).toUpperCase() + head.slice(1) : null; } @@ -138,7 +137,18 @@ export function polymorphicType(record, relationName, typeAttr) { /** The universe extension manager, through either of its service names. */ export function extensionManager(owner) { - return lookupService(owner, 'universe/extension-manager') ?? lookupService(owner, 'universe')?.extensionManager ?? null; + const direct = lookupService(owner, 'universe/extension-manager'); + + if (direct) { + return direct; + } + + // The universe service resolves its manager lazily and throws on a host that has none. + try { + return lookupService(owner, 'universe')?.extensionManager ?? null; + } catch { + return null; + } } /** Whether an engine is installed, so a resource it owns has somewhere to open. */ diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/adapters/application.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/adapters/application.js.html new file mode 100644 index 0000000..70224c2 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/adapters/application.js.html @@ -0,0 +1,796 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/adapters/application.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/adapters application.js

+
+ +
+ 47.91% + Statements + 23/48 +
+ + +
+ 25% + Branches + 8/32 +
+ + +
+ 33.33% + Functions + 3/9 +
+ + +
+ 47.91% + Lines + 23/48 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +86x +  +86x +86x +86x +  +  +  +  +  +  +  +  +86x +86x +86x +86x +  +  +  +86x +86x +86x +  +  +86x +86x +86x +  +  +  +  +  +  +  +  +  +  +86x +  +86x +  +  +  +86x +  +  +  +86x +  +  +  +86x +86x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +23x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import RESTAdapter from '@ember-data/adapter/rest';
+import AdapterError from '@ember-data/adapter/error';
+import { tracked } from '@glimmer/tracking';
+import { inject as service } from '@ember/service';
+import { storageFor } from 'ember-local-storage';
+import { get } from '@ember/object';
+import { isBlank } from '@ember/utils';
+import { isArray } from '@ember/array';
+import { dasherize } from '@ember/string';
+import { pluralize } from 'ember-inflector';
+import { decompress as decompressJson } from 'compress-json';
+import getUserOptions from '../utils/get-user-options';
+import config from 'ember-get-config';
+ 
+/* istanbul ignore if -- runs at import; whichever of adapters/application or services/fetch loads first sets the host, so the other can never enter */
+if (isBlank(config.API.host)) {
+    config.API.host = `${window.location.protocol}//${window.location.hostname}`;
+}
+const DEFAULT_ERROR_MESSAGE = 'Oops! Something went wrong. Please try again or contact support if the issue persists.';
+export default class ApplicationAdapter extends RESTAdapter {
+    /**
+     * Inject the `session` service
+     *
+     * @var {Service}
+     */
+    @service session;
+ 
+    /**
+     * Inject the `currentUser` service
+     *
+     * @var {Service}
+     */
+    @service currentUser;
+ 
+    /**
+     * User options for setting specific headers
+     *
+     * @var StorageObject
+     */
+    @storageFor('user-options') userOptions;
+ 
+    /**
+     * The default namespace for the adapter
+     *
+     * @var {String}
+     */
+    @tracked host;
+ 
+    /**
+     * The default namespace for adapter
+     *
+     * @var {String}
+     */
+    @tracked namespace;
+ 
+    /**
+     * Credentials
+     *
+     * @var {String}
+     */
+    @tracked credentials = 'include';
+ 
+    /**
+     * Mutable headers property.
+     *
+     * @var {Array}
+     */
+    @tracked _headers;
+ 
+    /**
+     * Creates an instance of ApplicationAdapter.
+     * @memberof ApplicationAdapter
+     */
+    constructor() {
+        super(...arguments);
+ 
+        this.host = get(config, 'API.host');
+        this.namespace = get(config, 'API.namespace');
+        this.headers = this.setupHeaders();
+    }
+ 
+    /**
+     * Setup headers that should be sent with request.
+     *
+     * @return {Object}
+     */
+    setupHeaders() {
+        const headers = {};
+        const userId = this.session.data.authenticated.user;
+        const userOptions = getUserOptions();
+        const isSandbox = get(userOptions, `${userId}:sandbox`) === true;
+        // `currentUser.setOption` dasherizes before storing, so the key written
+        // by `setOption('testKey', …)` is `<user>:test-key`. Reading `testKey`
+        // never matched anything, so this header was never sent.
+        const testKey = get(userOptions, `${userId}:test-key`);
+        let isAuthenticated = this.session.isAuthenticated;
+        let { token } = this.session.data.authenticated;
+ 
+        // If the session data is not yet available, check localStorage
+        Eif (!isAuthenticated) {
+            const localStorageSession = JSON.parse(window.localStorage.getItem('ember_simple_auth-session'));
+            Iif (localStorageSession) {
+                const { authenticated } = localStorageSession;
+                if (authenticated) {
+                    token = authenticated.token;
+                }
+ 
+                // Check isAuthenticated again
+                isAuthenticated = !!token;
+            }
+        }
+ 
+        headers['Content-Type'] = 'application/json';
+ 
+        Iif (isAuthenticated) {
+            headers['Authorization'] = `Bearer ${token}`;
+        }
+ 
+        Iif (isAuthenticated && isSandbox) {
+            headers['Access-Console-Sandbox'] = true;
+        }
+ 
+        Iif (isAuthenticated && !isBlank(testKey)) {
+            headers['Access-Console-Sandbox-Key'] = testKey;
+        }
+ 
+        this.headers = headers;
+        return this.headers;
+    }
+ 
+    /**
+     * Configure AJAX options for request, return as options hash
+     *
+     * @param {String} url
+     * @param {String} type The request type GET, POST, PUT, DELETE etc.
+     * @param {Object} options
+     *
+     * @return {Object}
+     */
+    ajaxOptions(url, type, options) {
+        this.setupHeaders();
+ 
+        const ajaxOptions = super.ajaxOptions(url, type, options);
+        ajaxOptions.credentials = this.credentials;
+ 
+        return ajaxOptions;
+    }
+ 
+    /**
+     * Dasherize the path for type
+     *
+     * @param {Object} type
+     */
+    pathForType(type) {
+        return dasherize(pluralize(type));
+    }
+ 
+    /**
+     * Handles the response from an AJAX request.
+     * It decompresses the payload if needed, checks for error responses, and returns an AdapterError for error scenarios.
+     * For valid responses, the handling is delegated to the superclass's handleResponse method.
+     *
+     * @param {number} status - HTTP status code of the response.
+     * @param {object} headers - Response headers.
+     * @param {object} payload - Response payload.
+     * @param {object} requestData - Original request data.
+     * @return {Object | AdapterError} - The response object or an AdapterError in case of errors.
+     */
+    handleResponse(status, headers, payload, requestData) {
+        const decompressedPayload = this.decompressPayload(payload, headers);
+        if (this.isErrorResponse(status, decompressedPayload)) {
+            const errors = this.getResponseErrors(decompressedPayload);
+            const errorMessage = this.getErrorMessage(errors);
+            return new AdapterError(errors, errorMessage);
+        }
+ 
+        return super.handleResponse(status, headers, decompressedPayload, requestData);
+    }
+ 
+    /**
+     * Decompresses the response payload if it's marked as compressed in the response headers.
+     *
+     * This method checks the response headers for a specific 'x-compressed-json' flag.
+     * If this flag is set, indicating that the response payload is compressed, the method
+     * decompresses the payload. The decompressed payload is then parsed as JSON and returned.
+     * If the payload is not compressed, it is returned as is.
+     *
+     * @param {object} payload - The original payload of the response.
+     * @param {object} headers - The headers of the response, used to check if the payload is compressed.
+     * @return {object} The decompressed payload if it was compressed, or the original payload otherwise.
+     */
+    decompressPayload(payload, headers) {
+        // Check if the response is compressed
+        if (headers['x-compressed-json'] === '1' || headers['x-compressed-json'] === 1) {
+            // Decompress the payload
+            const decompressedPayload = decompressJson(payload);
+            // Replace payload with decompressed json payload
+            payload = JSON.parse(decompressedPayload);
+        }
+ 
+        return payload;
+    }
+ 
+    /**
+     * Extracts the error message from a list of errors.
+     * Returns a default error message if the provided list is empty or undefined.
+     *
+     * @param {Array} errors - Array of error messages or objects.
+     * @return {string} - The extracted error message.
+     */
+    getErrorMessage(errors = []) {
+        return errors[0] ? errors[0] : DEFAULT_ERROR_MESSAGE;
+    }
+ 
+    /**
+     * Extracts errors from a payload.
+     * Assumes the payload contains an `errors` array; returns a default error message otherwise.
+     *
+     * @param {object} payload - The response payload.
+     * @return {Array} - Array of extracted errors or a default error message.
+     */
+    getResponseErrors(payload) {
+        return isArray(payload.errors) ? payload.errors : [DEFAULT_ERROR_MESSAGE];
+    }
+ 
+    /**
+     * Determines if the response status indicates an error.
+     * Checks both the HTTP status code and the presence of errors in the payload.
+     *
+     * @param {number} status - The HTTP status code.
+     * @param {object} payload - The response payload.
+     * @return {boolean} - True if the response indicates an error, false otherwise.
+     */
+    isErrorResponse(status, payload) {
+        return (status >= 400 && status < 600) || (!isBlank(payload) && isArray(payload.errors));
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/adapters/index.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/adapters/index.html new file mode 100644 index 0000000..be444ed --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/adapters/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/adapters + + + + + + + + + +
+
+

All files ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/adapters

+
+ +
+ 47.91% + Statements + 23/48 +
+ + +
+ 25% + Branches + 8/32 +
+ + +
+ 33.33% + Functions + 3/9 +
+ + +
+ 47.91% + Lines + 23/48 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
application.js +
+
47.91%23/4825%8/3233.33%3/947.91%23/48
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/base-contract.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/base-contract.js.html new file mode 100644 index 0000000..e961f73 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/base-contract.js.html @@ -0,0 +1,412 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/base-contract.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts base-contract.js

+
+ +
+ 0% + Statements + 0/10 +
+ + +
+ 0% + Branches + 0/4 +
+ + +
+ 0% + Functions + 0/9 +
+ + +
+ 0% + Lines + 0/10 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { tracked } from '@glimmer/tracking';
+ 
+/**
+ * Base class for all extension contracts
+ * Provides common functionality for validation, serialization, and option management
+ *
+ * Uses a two-phase construction pattern:
+ * 1. Constructor - Sets up initial state
+ * 2. setup() - Called after construction for validation and post-init logic
+ *
+ * @class BaseContract
+ */
+export default class BaseContract {
+    /* istanbul ignore next -- the constructor assigns _options unconditionally, so this default is never read */
+    @tracked _options = {};
+ 
+    constructor(options = {}) {
+        this._options = { ...options };
+        // Don't validate here - let subclasses set properties first
+    }
+ 
+    /**
+     * Setup method called after construction
+     * Subclasses should call super.setup() to trigger validation
+     *
+     * @method setup
+     */
+    setup() {
+        this.validate();
+    }
+ 
+    /**
+     * Validate the contract
+     * Override in subclasses to add specific validation logic
+     *
+     * @method validate
+     */
+    validate() {
+        // Base validation - override in subclasses
+    }
+ 
+    /**
+     * Get the plain object representation of this contract
+     *
+     * @method toObject
+     * @returns {Object} Plain object representation
+     */
+    toObject() {
+        return { ...this._options };
+    }
+ 
+    /**
+     * Set an option with method chaining support
+     *
+     * @method setOption
+     * @param {String} key The option key
+     * @param {*} value The option value
+     * @returns {BaseContract} This instance for chaining
+     */
+    setOption(key, value) {
+        this._options[key] = value;
+        return this;
+    }
+ 
+    /**
+     * Get an option value
+     *
+     * @method getOption
+     * @param {String} key The option key
+     * @param {*} defaultValue Default value if option doesn't exist
+     * @returns {*} The option value or default
+     */
+    getOption(key, defaultValue = null) {
+        return this._options[key] !== undefined ? this._options[key] : defaultValue;
+    }
+ 
+    /**
+     * Check if an option exists
+     *
+     * @method hasOption
+     * @param {String} key The option key
+     * @returns {Boolean} True if option exists
+     */
+    hasOption(key) {
+        return this._options[key] !== undefined;
+    }
+ 
+    /**
+     * Remove an option
+     *
+     * @method removeOption
+     * @param {String} key The option key
+     * @returns {BaseContract} This instance for chaining
+     */
+    removeOption(key) {
+        delete this._options[key];
+        return this;
+    }
+ 
+    /**
+     * Get all options
+     *
+     * @method getOptions
+     * @returns {Object} All options
+     */
+    getOptions() {
+        return { ...this._options };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/extension-component.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/extension-component.js.html new file mode 100644 index 0000000..6a6d940 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/extension-component.js.html @@ -0,0 +1,604 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/extension-component.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts extension-component.js

+
+ +
+ 0% + Statements + 0/38 +
+ + +
+ 0% + Branches + 0/19 +
+ + +
+ 0% + Functions + 0/8 +
+ + +
+ 0% + Lines + 0/38 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import BaseContract from './base-contract';
+ 
+/**
+ * Represents a lazy-loadable component from an engine
+ *
+ * This contract defines a component that will be loaded on-demand from an engine,
+ * preserving lazy loading capabilities while allowing cross-engine component usage.
+ *
+ * @class ExtensionComponent
+ * @extends BaseContract
+ *
+ * @example
+ * // Simple usage
+ * new ExtensionComponent('@fleetbase/fleetops-engine', 'components/admin/navigator-app')
+ *
+ * @example
+ * // With options
+ * new ExtensionComponent('@fleetbase/fleetops-engine', {
+ *   path: 'components/admin/navigator-app',
+ *   loadingComponent: 'loading-spinner',
+ *   errorComponent: 'error-display'
+ * })
+ *
+ * @example
+ * // With method chaining
+ * new ExtensionComponent('@fleetbase/fleetops-engine', 'components/widget/metrics')
+ *   .withLoadingComponent('skeletons/widget')
+ *   .withErrorComponent('error-boundary')
+ *   .withData({ refreshInterval: 5000 })
+ */
+export default class ExtensionComponent extends BaseContract {
+    /**
+     * Create a new ExtensionComponent
+     *
+     * @constructor
+     * @param {String} engineName The name of the engine (e.g., '@fleetbase/fleetops-engine')
+     * @param {String|Function|Object} pathClassOrOptions Component path, component class, or options object
+     */
+    constructor(engineName, pathClassOrOptions = {}) {
+        // Handle component class
+        if (typeof pathClassOrOptions === 'function' && pathClassOrOptions.prototype) {
+            const componentClass = pathClassOrOptions;
+            super({
+                engine: engineName,
+                class: componentClass,
+                name: componentClass.name,
+            });
+ 
+            this.engine = engineName;
+            this.class = componentClass;
+            this.name = componentClass.name;
+            this.path = null; // No path for classes
+            this.isClass = true;
+            this.loadingComponent = null;
+            this.errorComponent = null;
+            return;
+        }
+ 
+        // Handle string path or options object
+        const options = typeof pathClassOrOptions === 'string' ? { path: pathClassOrOptions } : pathClassOrOptions;
+ 
+        super({
+            engine: engineName,
+            ...options,
+        });
+ 
+        this.engine = engineName;
+        this.path = options.path;
+        this.name = options.path; // Add name for parity
+        this.class = null;
+        this.isClass = false;
+        this.loadingComponent = options.loadingComponent || null;
+        this.errorComponent = options.errorComponent || null;
+    }
+ 
+    /**
+     * Validate the component definition
+     *
+     * @method validate
+     * @throws {Error} If engine name or path/class is missing
+     */
+    validate() {
+        if (!this.engine) {
+            throw new Error('ExtensionComponent requires an engine name');
+        }
+        if (!this.path && !this.class) {
+            throw new Error('ExtensionComponent requires a component path or class');
+        }
+    }
+ 
+    /**
+     * Set a custom loading component to display while the engine loads
+     *
+     * @method withLoadingComponent
+     * @param {String} componentName Name of the loading component
+     * @returns {ExtensionComponent} This instance for chaining
+     */
+    withLoadingComponent(componentName) {
+        this.loadingComponent = componentName;
+        this._options.loadingComponent = componentName;
+        return this;
+    }
+ 
+    /**
+     * Set a custom error component to display if loading fails
+     *
+     * @method withErrorComponent
+     * @param {String} componentName Name of the error component
+     * @returns {ExtensionComponent} This instance for chaining
+     */
+    withErrorComponent(componentName) {
+        this.errorComponent = componentName;
+        this._options.errorComponent = componentName;
+        return this;
+    }
+ 
+    /**
+     * Add custom data to pass to the component when it renders
+     *
+     * @method withData
+     * @param {Object} data Custom data object
+     * @returns {ExtensionComponent} This instance for chaining
+     */
+    withData(data) {
+        this._options.data = data;
+        return this;
+    }
+ 
+    /**
+     * Set a timeout for loading the component
+     *
+     * @method withTimeout
+     * @param {Number} milliseconds Timeout in milliseconds
+     * @returns {ExtensionComponent} This instance for chaining
+     */
+    withTimeout(milliseconds) {
+        this._options.timeout = milliseconds;
+        return this;
+    }
+ 
+    /**
+     * Get the plain object representation
+     *
+     * @method toObject
+     * @returns {Object} Plain object with all component definition properties
+     */
+    toObject() {
+        return {
+            engine: this.engine,
+            path: this.path,
+            name: this.name,
+            class: this.class,
+            isClass: this.isClass,
+            loadingComponent: this.loadingComponent,
+            errorComponent: this.errorComponent,
+            ...this._options,
+        };
+    }
+ 
+    /**
+     * Get the string representation
+     *
+     * @method toString
+     * @returns {String} Plain string component definition properties
+     */
+    toString() {
+        if (this.isClass) {
+            return `#extension-component:${this.engine}:${this.name}`;
+        }
+ 
+        return `#extension-component:${this.engine}:${this.path}`;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/hook-registry.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/hook-registry.js.html new file mode 100644 index 0000000..abe19fb --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/hook-registry.js.html @@ -0,0 +1,139 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/hook-registry.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts hook-registry.js

+
+ +
+ 0% + Statements + 0/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { tracked } from '@glimmer/tracking';
+ 
+/**
+ * HookRegistry
+ *
+ * Shared registry object for application hooks.
+ * Registered as a singleton in the application container to ensure
+ * all HookService instances (app and engines) share the same hooks.
+ *
+ * @class HookRegistry
+ */
+export default class HookRegistry {
+    /**
+     * Map of hook names to arrays of hook objects
+     * @type {Object}
+     */
+    @tracked hooks = {};
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/hook.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/hook.js.html new file mode 100644 index 0000000..a040416 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/hook.js.html @@ -0,0 +1,715 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/hook.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts hook.js

+
+ +
+ 0% + Statements + 0/39 +
+ + +
+ 0% + Branches + 0/35 +
+ + +
+ 0% + Functions + 0/11 +
+ + +
+ 0% + Lines + 0/39 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import BaseContract from './base-contract';
+import { guidFor } from '@ember/object/internals';
+import isObject from '../utils/is-object';
+ 
+/**
+ * Represents a lifecycle or application hook
+ *
+ * Hooks allow extensions to inject custom logic at specific points in the application lifecycle.
+ *
+ * @class Hook
+ * @extends BaseContract
+ *
+ * @example
+ * // Simple hook with chaining
+ * new Hook('application:before-model', (session, router) => {
+ *   if (session.isCustomer) {
+ *     router.transitionTo('customer-portal');
+ *   }
+ * })
+ *
+ * @example
+ * // Full definition object (first-class)
+ * new Hook({
+ *   name: 'application:before-model',
+ *   handler: (session, router) => {
+ *     if (session.isCustomer) {
+ *       router.transitionTo('customer-portal');
+ *     }
+ *   },
+ *   priority: 10,
+ *   once: false,
+ *   id: 'customer-redirect'
+ * })
+ *
+ * @example
+ * // Hook with method chaining
+ * new Hook('order:before-save')
+ *   .withPriority(10)
+ *   .once()
+ *   .execute(async (order) => {
+ *     await validateOrder(order);
+ *   })
+ */
+export default class Hook extends BaseContract {
+    /**
+     * Create a new Hook
+     *
+     * @constructor
+     * @param {String|Object} nameOrDefinition Hook name or full definition object
+     * @param {Function} handlerOrOptions Handler function or options object (only used if first param is string)
+     */
+    constructor(nameOrDefinition, handlerOrOptions = null) {
+        // Prepare options for super
+        const options = typeof handlerOrOptions === 'function' ? { handler: handlerOrOptions } : handlerOrOptions || {};
+ 
+        // Call super FIRST (JavaScript requirement)
+        super(isObject(nameOrDefinition) && nameOrDefinition.name ? nameOrDefinition : { name: nameOrDefinition, ...options });
+ 
+        // THEN set properties
+        if (isObject(nameOrDefinition) && nameOrDefinition.name) {
+            const definition = nameOrDefinition;
+ 
+            this.name = definition.name;
+            this.handler = definition.handler || null;
+            this.priority = definition.priority !== undefined ? definition.priority : 0;
+            this.runOnce = definition.once || false;
+            this.id = definition.id || guidFor(this);
+            this.enabled = definition.enabled !== undefined ? definition.enabled : true;
+        } else {
+            this.name = nameOrDefinition;
+            this.handler = options.handler || null;
+            this.priority = options.priority || 0;
+            this.runOnce = options.once || false;
+            this.id = options.id || guidFor(this);
+            this.enabled = options.enabled !== undefined ? options.enabled : true;
+        }
+ 
+        // Call setup() to trigger validation after properties are set
+        super.setup();
+    }
+ 
+    /**
+     * Validate the hook
+     *
+     * @method validate
+     * @throws {Error} If name is missing
+     */
+    validate() {
+        if (!this.name) {
+            throw new Error('Hook requires a name');
+        }
+    }
+ 
+    /**
+     * Set the hook handler function
+     *
+     * @method execute
+     * @param {Function} handler The handler function
+     * @returns {Hook} This instance for chaining
+     */
+    execute(handler) {
+        this.handler = handler;
+        this._options.handler = handler;
+        return this;
+    }
+ 
+    /**
+     * Set the hook priority
+     * Lower numbers execute first
+     *
+     * @method withPriority
+     * @param {Number} priority Priority value
+     * @returns {Hook} This instance for chaining
+     */
+    withPriority(priority) {
+        this.priority = priority;
+        this._options.priority = priority;
+        return this;
+    }
+ 
+    /**
+     * Mark this hook to run only once
+     * After execution, it will be automatically removed
+     *
+     * @method once
+     * @returns {Hook} This instance for chaining
+     */
+    once() {
+        this.runOnce = true;
+        this._options.once = true;
+        return this;
+    }
+ 
+    /**
+     * Set a unique ID for this hook
+     * Useful for removing specific hooks later
+     *
+     * @method withId
+     * @param {String} id Unique identifier
+     * @returns {Hook} This instance for chaining
+     */
+    withId(id) {
+        this.id = id;
+        this._options.id = id;
+        return this;
+    }
+ 
+    /**
+     * Enable or disable the hook
+     *
+     * @method setEnabled
+     * @param {Boolean} enabled Whether the hook is enabled
+     * @returns {Hook} This instance for chaining
+     */
+    setEnabled(enabled) {
+        this.enabled = enabled;
+        this._options.enabled = enabled;
+        return this;
+    }
+ 
+    /**
+     * Disable the hook
+     *
+     * @method disable
+     * @returns {Hook} This instance for chaining
+     */
+    disable() {
+        return this.setEnabled(false);
+    }
+ 
+    /**
+     * Enable the hook
+     *
+     * @method enable
+     * @returns {Hook} This instance for chaining
+     */
+    enable() {
+        return this.setEnabled(true);
+    }
+ 
+    /**
+     * Add metadata to the hook
+     *
+     * @method withMetadata
+     * @param {Object} metadata Metadata object
+     * @returns {Hook} This instance for chaining
+     */
+    withMetadata(metadata) {
+        this._options.metadata = metadata;
+        return this;
+    }
+ 
+    /**
+     * Get the plain object representation
+     *
+     * @method toObject
+     * @returns {Object} Plain object with all hook properties
+     */
+    toObject() {
+        return {
+            name: this.name,
+            handler: this.handler,
+            priority: this.priority,
+            once: this.runOnce,
+            id: this.id,
+            enabled: this.enabled,
+            ...this._options,
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/index.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/index.html new file mode 100644 index 0000000..ced228a --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/index.html @@ -0,0 +1,236 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts + + + + + + + + + +
+
+

All files ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts

+
+ +
+ 0% + Statements + 0/335 +
+ + +
+ 0% + Branches + 0/213 +
+ + +
+ 0% + Functions + 0/72 +
+ + +
+ 0% + Lines + 0/334 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
base-contract.js +
+
0%0/100%0/40%0/90%0/10
extension-component.js +
+
0%0/380%0/190%0/80%0/38
hook-registry.js +
+
0%0/1100%0/0100%0/00%0/1
hook.js +
+
0%0/390%0/350%0/110%0/39
menu-item.js +
+
0%0/1320%0/980%0/200%0/132
menu-panel.js +
+
0%0/350%0/230%0/90%0/34
template-helper.js +
+
0%0/180%0/40%0/10%0/18
universe-registry.js +
+
0%0/1100%0/0100%0/00%0/1
widget.js +
+
0%0/610%0/300%0/140%0/61
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/menu-item.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/menu-item.js.html new file mode 100644 index 0000000..3682f4d --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/menu-item.js.html @@ -0,0 +1,1834 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/menu-item.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts menu-item.js

+
+ +
+ 0% + Statements + 0/132 +
+ + +
+ 0% + Branches + 0/98 +
+ + +
+ 0% + Functions + 0/20 +
+ + +
+ 0% + Lines + 0/132 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import BaseContract from './base-contract';
+import ExtensionComponent from './extension-component';
+import { dasherize } from '@ember/string';
+import { isArray } from '@ember/array';
+import isObject from '../utils/is-object';
+ 
+/**
+ * Represents a menu item in the application
+ *
+ * Menu items can be simple navigation links or complex interactive components.
+ * They support routing, icons, priorities, click handlers, and lazy-loaded components.
+ *
+ * @class MenuItem
+ * @extends BaseContract
+ *
+ * @example
+ * // Simple menu item with chaining
+ * new MenuItem('Fleet-Ops', 'console.fleet-ops')
+ *   .withIcon('route')
+ *   .withPriority(0)
+ *
+ * @example
+ * // Full definition object (first-class)
+ * new MenuItem({
+ *   title: 'Fleet-Ops',
+ *   route: 'console.fleet-ops',
+ *   icon: 'route',
+ *   priority: 0,
+ *   component: { engine: '@fleetbase/fleetops-engine', path: 'components/admin/navigator-app' }
+ * })
+ *
+ * @example
+ * // Menu item with component
+ * new MenuItem('Settings')
+ *   .withComponent(new ExtensionComponent('@fleetbase/my-engine', 'components/settings'))
+ *   .withOnClick((menuItem, router) => {
+ *     router.transitionTo('virtual', menuItem.slug);
+ *   })
+ *
+ * @example
+ * // Menu item with description and shortcuts (Phase 2)
+ * new MenuItem('Fleet-Ops', 'console.fleet-ops')
+ *   .withIcon('route')
+ *   .withDescription('Manage fleets, drivers, and live orders')
+ *   .withShortcuts([
+ *     { title: 'Scheduler', route: 'console.fleet-ops.scheduler', icon: 'calendar' },
+ *     { title: 'Order Config', route: 'console.fleet-ops.order-configs', icon: 'gear' },
+ *   ])
+ */
+export default class MenuItem extends BaseContract {
+    /**
+     * Create a new MenuItem
+     *
+     * @constructor
+     * @param {String|Object} titleOrDefinition The menu item title or full definition object
+     * @param {String} route Optional route name (only used if first param is string)
+     */
+    constructor(titleOrDefinition, route = null) {
+        // Call super FIRST (JavaScript requirement)
+        super(isObject(titleOrDefinition) ? titleOrDefinition : { title: titleOrDefinition, route });
+ 
+        // THEN set properties
+        if (isObject(titleOrDefinition)) {
+            const definition = titleOrDefinition;
+ 
+            // Core properties
+            this.title = definition.title || null;
+            this.text = definition.text || definition.title;
+            this.label = definition.label || definition.title;
+            this.id = definition.id || (definition.title ? dasherize(definition.title) : null);
+            this.slug = definition.slug || (this.title ? dasherize(this.title) : null);
+ 
+            // Routing properties
+            this.route = definition.route || null;
+            this.section = definition.section || null;
+            this.queryParams = definition.queryParams || {};
+            this.routeParams = definition.routeParams || [];
+            this.view = definition.view || (this.title ? dasherize(this.title) : null);
+ 
+            // Display properties
+            this.icon = definition.icon || 'circle-dot';
+            this.iconComponent = definition.iconComponent || null;
+            this.iconComponentOptions = definition.iconComponentOptions || {};
+            this.iconSize = definition.iconSize || null;
+            this.iconPrefix = definition.iconPrefix || null;
+            this.iconClass = definition.iconClass || null;
+ 
+            // Component properties
+            this.component = definition.component || null;
+            this.componentParams = definition.componentParams || {};
+            this.renderComponentInPlace = definition.renderComponentInPlace || false;
+ 
+            // Styling properties
+            this.class = definition.class || null;
+            this.inlineClass = definition.inlineClass || null;
+            this.wrapperClass = definition.wrapperClass || null;
+            this.overwriteWrapperClass = definition.overwriteWrapperClass || false;
+ 
+            // Behavior properties
+            this.priority = definition.priority !== undefined ? definition.priority : 9;
+            this.index = definition.index !== undefined ? definition.index : 0;
+            this.type = definition.type || 'default';
+            this.buttonType = definition.buttonType || null;
+            this.onClick = definition.onClick || null;
+ 
+            // State properties
+            this.disabled = definition.disabled || false;
+            this.isLoading = definition.isLoading || false;
+ 
+            // Permission and i18n
+            this.permission = definition.permission || null;
+            this.intl = definition.intl || null;
+ 
+            // Nested items
+            this.items = definition.items || null;
+ 
+            // ── Phase 2 additions ──────────────────────────────────────────
+            // A short human-readable description of the extension shown in the
+            // overflow dropdown.  Optional – defaults to null.
+            this.description = definition.description || null;
+ 
+            // An array of shortcut items (sub-menu links) displayed beneath the
+            // extension name in the multi-column dropdown.  Each shortcut is a
+            // plain object with at minimum { title, route } and optionally
+            // { icon, iconPrefix, id }.  Optional – defaults to null.
+            this.shortcuts = definition.shortcuts || null;
+ 
+            // An array of string tags used to improve search discoverability in
+            // the overflow dropdown.  e.g. ['logistics', 'tracking', 'fleet'].
+            // Optional – defaults to null.
+            this.tags = isArray(definition.tags) ? definition.tags : definition.tags ? [definition.tags] : null;
+        } else {
+            // Handle string title with optional route (chaining pattern)
+            this.title = titleOrDefinition;
+            this.text = titleOrDefinition;
+            this.label = titleOrDefinition;
+            this.id = dasherize(titleOrDefinition);
+            this.slug = dasherize(titleOrDefinition);
+ 
+            // Routing properties
+            this.route = route;
+            this.section = null;
+            this.queryParams = {};
+            this.routeParams = [];
+            this.view = dasherize(titleOrDefinition);
+ 
+            // Display properties
+            this.icon = 'circle-dot';
+            this.iconComponent = null;
+            this.iconComponentOptions = {};
+            this.iconSize = null;
+            this.iconPrefix = null;
+            this.iconClass = null;
+ 
+            // Component properties
+            this.component = null;
+            this.componentParams = {};
+            this.renderComponentInPlace = false;
+ 
+            // Styling properties
+            this.class = null;
+            this.inlineClass = null;
+            this.wrapperClass = null;
+            this.overwriteWrapperClass = false;
+ 
+            // Behavior properties
+            this.priority = 9;
+            this.index = 0;
+            this.type = 'default';
+            this.buttonType = null;
+            this.onClick = null;
+ 
+            // State properties
+            this.disabled = false;
+            this.isLoading = false;
+ 
+            // Permission and i18n
+            this.permission = null;
+            this.intl = null;
+ 
+            // Nested items
+            this.items = null;
+ 
+            // ── Phase 2 additions ──────────────────────────────────────────
+            this.description = null;
+            this.shortcuts = null;
+            this.tags = null;
+        }
+ 
+        // Call setup() to trigger validation after properties are set
+        super.setup();
+    }
+ 
+    /**
+     * Validate the menu item
+     *
+     * @method validate
+     * @throws {Error} If title is missing
+     */
+    validate() {
+        if (!this.title) {
+            throw new Error('MenuItem requires a title');
+        }
+    }
+ 
+    /**
+     * Set the menu item icon
+     *
+     * @method withIcon
+     * @param {String} icon Icon name (FontAwesome or custom)
+     * @returns {MenuItem} This instance for chaining
+     */
+    withIcon(icon) {
+        this.icon = icon;
+        this._options.icon = icon;
+        return this;
+    }
+ 
+    /**
+     * Set the menu item priority
+     * Lower numbers appear first in the menu
+     *
+     * @method withPriority
+     * @param {Number} priority Priority value (default: 9)
+     * @returns {MenuItem} This instance for chaining
+     */
+    withPriority(priority) {
+        this.priority = priority;
+        this._options.priority = priority;
+        return this;
+    }
+ 
+    /**
+     * Set a component for the menu item
+     *
+     * @method withComponent
+     * @param {ExtensionComponent|Object} component Component definition
+     * @returns {MenuItem} This instance for chaining
+     */
+    withComponent(component) {
+        if (component instanceof ExtensionComponent) {
+            this.component = component.toObject();
+        } else {
+            this.component = component;
+        }
+        this._options.component = this.component;
+        return this;
+    }
+ 
+    /**
+     * Set a click handler for the menu item.
+     *
+     * `onClick` itself is the handler property every consumer calls, so the setter
+     * cannot share its name: an instance's `onClick` field would shadow it.
+     *
+     * @method withOnClick
+     * @param {Function} handler Click handler function
+     * @returns {MenuItem} This instance for chaining
+     */
+    withOnClick(handler) {
+        this.onClick = handler;
+        this._options.onClick = handler;
+        return this;
+    }
+ 
+    /**
+     * Set the menu item slug
+     *
+     * @method withSlug
+     * @param {String} slug URL-friendly slug
+     * @returns {MenuItem} This instance for chaining
+     */
+    withSlug(slug) {
+        this.slug = slug;
+        this._options.slug = slug;
+        return this;
+    }
+ 
+    /**
+     * Set query parameters for the route
+     *
+     * @method withQueryParams
+     * @param {Object} params Query parameters object
+     * @returns {MenuItem} This instance for chaining
+     */
+    withQueryParams(params) {
+        this.queryParams = params;
+        this._options.queryParams = params;
+        return this;
+    }
+ 
+    /**
+     * Set route parameters
+     *
+     * @method withRouteParams
+     * @param {...*} params Route parameters
+     * @returns {MenuItem} This instance for chaining
+     */
+    withRouteParams(...params) {
+        this.routeParams = params;
+        this._options.routeParams = params;
+        return this;
+    }
+ 
+    /**
+     * Set the section this menu item belongs to
+     *
+     * @method inSection
+     * @param {String} section Section name
+     * @returns {MenuItem} This instance for chaining
+     */
+    inSection(section) {
+        this.section = section;
+        this._options.section = section;
+        return this;
+    }
+ 
+    /**
+     * Set the index position within its section
+     *
+     * @method atIndex
+     * @param {Number} index Index position
+     * @returns {MenuItem} This instance for chaining
+     */
+    atIndex(index) {
+        this.index = index;
+        this._options.index = index;
+        return this;
+    }
+ 
+    /**
+     * Set the menu item type
+     *
+     * @method withType
+     * @param {String} type Type (e.g., 'link', 'button', 'default')
+     * @returns {MenuItem} This instance for chaining
+     */
+    withType(type) {
+        this.type = type;
+        this._options.type = type;
+        return this;
+    }
+ 
+    /**
+     * Set wrapper CSS class
+     *
+     * @method withWrapperClass
+     * @param {String} wrapperClass CSS class for wrapper element
+     * @returns {MenuItem} This instance for chaining
+     */
+    withWrapperClass(wrapperClass) {
+        this.wrapperClass = wrapperClass;
+        this._options.wrapperClass = wrapperClass;
+        return this;
+    }
+ 
+    /**
+     * Set component parameters
+     *
+     * @method withComponentParams
+     * @param {Object} params Parameters to pass to component
+     * @returns {MenuItem} This instance for chaining
+     */
+    withComponentParams(params) {
+        this._options.componentParams = params;
+        return this;
+    }
+ 
+    /**
+     * Set whether to render component in place
+     *
+     * @method renderInPlace
+     * @param {Boolean} inPlace Whether to render in place
+     * @returns {MenuItem} This instance for chaining
+     */
+    renderInPlace(inPlace = true) {
+        this._options.renderComponentInPlace = inPlace;
+        return this;
+    }
+ 
+    // ── Phase 2 builder methods ────────────────────────────────────────────
+ 
+    /**
+     * Set a short human-readable description for the extension.
+     * Displayed beneath the extension title in the overflow dropdown.
+     *
+     * @method withDescription
+     * @param {String} description Short description text
+     * @returns {MenuItem} This instance for chaining
+     *
+     * @example
+     * new MenuItem('Fleet-Ops', 'console.fleet-ops')
+     *   .withDescription('Manage fleets, drivers, and live orders')
+     */
+    withDescription(description) {
+        this.description = description;
+        this._options.description = description;
+        return this;
+    }
+ 
+    /**
+     * Set an array of shortcut items displayed as independent sibling cards in
+     * the multi-column overflow dropdown (AWS Console style).  Each shortcut
+     * supports the full MenuItem property surface:
+     *
+     *   Required:
+     *     title    {String}   Display label
+     *
+     *   Routing:
+     *     route    {String}   Ember route name
+     *     queryParams {Object}
+     *     routeParams {Array}
+     *
+     *   Identity:
+     *     id       {String}   Explicit id (auto-dasherized from title if omitted)
+     *     slug     {String}   URL slug (falls back to id)
+     *
+     *   Icons:
+     *     icon            {String}   FontAwesome icon name
+     *     iconPrefix      {String}   FA prefix (e.g. 'far', 'fab')
+     *     iconSize        {String}   FA size string
+     *     iconClass       {String}   Extra CSS class on the icon element
+     *     iconComponent   {String}   Lazy-loaded engine component path
+     *     iconComponentOptions {Object}
+     *
+     *   Metadata:
+     *     description {String}   Short description shown in the card
+     *     tags        {String[]} Search tags
+     *
+     *   Behaviour:
+     *     onClick  {Function}  Click handler (receives the shortcut item)
+     *     disabled {Boolean}
+     *
+     * Shortcuts are registered as first-class header menu items at boot time
+     * and can be individually pinned to the navigation bar.
+     *
+     * @method withShortcuts
+     * @param {Array<Object>} shortcuts Array of shortcut definition objects
+     * @returns {MenuItem} This instance for chaining
+     *
+     * @example
+     * new MenuItem('Fleet-Ops', 'console.fleet-ops')
+     *   .withShortcuts([
+     *     {
+     *       title: 'Scheduler',
+     *       route: 'console.fleet-ops.scheduler',
+     *       icon: 'calendar',
+     *       description: 'Plan and visualise driver schedules',
+     *       tags: ['schedule', 'calendar'],
+     *     },
+     *     {
+     *       title: 'Live Map',
+     *       route: 'console.fleet-ops',
+     *       iconComponent: 'fleet-ops@components/live-map-icon',
+     *       description: 'Real-time vehicle tracking',
+     *     },
+     *   ])
+     */
+    withShortcuts(shortcuts) {
+        this.shortcuts = isArray(shortcuts) ? shortcuts : null;
+        this._options.shortcuts = this.shortcuts;
+        return this;
+    }
+ 
+    /**
+     * Set an array of string tags for this menu item.
+     * Tags are matched against the search query in the overflow dropdown,
+     * making items discoverable even when the query doesn't match the title
+     * or description.
+     *
+     * @method withTags
+     * @param {String|String[]} tags One tag string or an array of tag strings
+     * @returns {MenuItem} This instance for chaining
+     *
+     * @example
+     * new MenuItem('Fleet-Ops', 'console.fleet-ops')
+     *   .withTags(['logistics', 'tracking', 'fleet', 'drivers'])
+     */
+    withTags(tags) {
+        this.tags = isArray(tags) ? tags : tags ? [tags] : null;
+        this._options.tags = this.tags;
+        return this;
+    }
+ 
+    /**
+     * Add a single shortcut to the existing shortcuts array.
+     * Creates the array if it does not yet exist.
+     *
+     * @method addShortcut
+     * @param {Object} shortcut Shortcut definition object { title, route, icon?, iconPrefix?, id? }
+     * @returns {MenuItem} This instance for chaining
+     *
+     * @example
+     * new MenuItem('Fleet-Ops', 'console.fleet-ops')
+     *   .addShortcut({ title: 'Scheduler', route: 'console.fleet-ops.scheduler' })
+     *   .addShortcut({ title: 'Order Config', route: 'console.fleet-ops.order-configs' })
+     */
+    addShortcut(shortcut) {
+        if (!isArray(this.shortcuts)) {
+            this.shortcuts = [];
+        }
+        this.shortcuts = [...this.shortcuts, shortcut];
+        this._options.shortcuts = this.shortcuts;
+        return this;
+    }
+ 
+    /**
+     * Get the plain object representation
+     *
+     * @method toObject
+     * @returns {Object} Plain object with all menu item properties
+     */
+    toObject() {
+        return {
+            // Core properties
+            id: this.id,
+            title: this.title,
+            text: this.text,
+            label: this.label,
+            slug: this.slug,
+ 
+            // Routing properties
+            route: this.route,
+            section: this.section,
+            view: this.view,
+            queryParams: this.queryParams,
+            routeParams: this.routeParams,
+ 
+            // Display properties
+            icon: this.icon,
+            iconComponent: this.iconComponent,
+            iconComponentOptions: this.iconComponentOptions,
+            iconSize: this.iconSize,
+            iconPrefix: this.iconPrefix,
+            iconClass: this.iconClass,
+ 
+            // Component properties
+            component: this.component,
+            componentParams: this.componentParams,
+            renderComponentInPlace: this.renderComponentInPlace,
+ 
+            // Styling properties
+            class: this.class,
+            inlineClass: this.inlineClass,
+            wrapperClass: this.wrapperClass,
+            overwriteWrapperClass: this.overwriteWrapperClass,
+ 
+            // Behavior properties
+            priority: this.priority,
+            index: this.index,
+            type: this.type,
+            buttonType: this.buttonType,
+            onClick: this.onClick,
+ 
+            // State properties
+            disabled: this.disabled,
+            isLoading: this.isLoading,
+ 
+            // Permission and i18n
+            permission: this.permission,
+            intl: this.intl,
+ 
+            // Nested items
+            items: this.items,
+ 
+            // ── Phase 2 additions ──────────────────────────────────────────
+            // Optional short description shown in the overflow dropdown card
+            description: this.description,
+ 
+            // Optional array of shortcut sub-links shown inside the extension card
+            shortcuts: this.shortcuts,
+ 
+            // Optional array of string tags for search discoverability
+            tags: this.tags,
+ 
+            // Indicator flag
+            _isMenuItem: true,
+ 
+            // Include any additional options
+            ...this._options,
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/menu-panel.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/menu-panel.js.html new file mode 100644 index 0000000..87161d6 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/menu-panel.js.html @@ -0,0 +1,592 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/menu-panel.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts menu-panel.js

+
+ +
+ 0% + Statements + 0/35 +
+ + +
+ 0% + Branches + 0/23 +
+ + +
+ 0% + Functions + 0/9 +
+ + +
+ 0% + Lines + 0/34 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import BaseContract from './base-contract';
+import MenuItem from './menu-item';
+import { dasherize } from '@ember/string';
+import isObject from '../utils/is-object';
+ 
+/**
+ * Represents a menu panel containing multiple menu items
+ *
+ * Menu panels are used in admin settings and other sections to group related menu items.
+ *
+ * @class MenuPanel
+ * @extends BaseContract
+ *
+ * @example
+ * // With chaining
+ * new MenuPanel('Fleet-Ops Config')
+ *   .withSlug('fleet-ops')
+ *   .withIcon('truck')
+ *   .addItem(new MenuItem('Navigator App').withIcon('location-arrow'))
+ *   .addItem(new MenuItem('Avatar Management').withIcon('images'))
+ *
+ * @example
+ * // Full definition object (first-class)
+ * new MenuPanel({
+ *   title: 'Fleet-Ops Config',
+ *   slug: 'fleet-ops',
+ *   icon: 'truck',
+ *   priority: 5,
+ *   items: [
+ *     { title: 'Navigator App', icon: 'location-arrow' },
+ *     { title: 'Avatar Management', icon: 'images' }
+ *   ]
+ * })
+ */
+export default class MenuPanel extends BaseContract {
+    /**
+     * Create a new MenuPanel
+     *
+     * @constructor
+     * @param {String|Object} titleOrDefinition The panel title or full definition object
+     * @param {Array} items Optional array of menu items (only used if first param is string)
+     */
+    constructor(titleOrDefinition, items = []) {
+        // Call super FIRST (JavaScript requirement)
+        super(isObject(titleOrDefinition) && titleOrDefinition.title ? titleOrDefinition : { title: titleOrDefinition });
+ 
+        // THEN set properties
+        if (isObject(titleOrDefinition) && titleOrDefinition.title) {
+            const definition = titleOrDefinition;
+ 
+            this.title = definition.title;
+            this.items = definition.items || [];
+            this.slug = definition.slug || dasherize(this.title);
+            this.icon = definition.icon || null;
+            this.open = definition.open !== undefined ? definition.open : true;
+            this.priority = definition.priority !== undefined ? definition.priority : 9;
+        } else {
+            // Handle string title (chaining pattern)
+            this.title = titleOrDefinition;
+            this.items = items;
+            this.slug = dasherize(titleOrDefinition);
+            this.icon = null;
+            this.open = true;
+            this.priority = 9;
+        }
+ 
+        // Call setup() to trigger validation after properties are set
+        super.setup();
+    }
+ 
+    /**
+     * Validate the menu panel
+     *
+     * @method validate
+     * @throws {Error} If title is missing
+     */
+    validate() {
+        if (!this.title) {
+            throw new Error('MenuPanel requires a title');
+        }
+    }
+ 
+    /**
+     * Set the panel slug
+     *
+     * @method withSlug
+     * @param {String} slug URL-friendly slug
+     * @returns {MenuPanel} This instance for chaining
+     */
+    withSlug(slug) {
+        this.slug = slug;
+        this._options.slug = slug;
+        return this;
+    }
+ 
+    /**
+     * Set the panel icon
+     *
+     * @method withIcon
+     * @param {String} icon Icon name
+     * @returns {MenuPanel} This instance for chaining
+     */
+    withIcon(icon) {
+        this.icon = icon;
+        this._options.icon = icon;
+        return this;
+    }
+ 
+    /**
+     * Set the panel priority
+     *
+     * @method withPriority
+     * @param {Number} priority Priority value
+     * @returns {MenuPanel} This instance for chaining
+     */
+    withPriority(priority) {
+        this.priority = priority;
+        this._options.priority = priority;
+        return this;
+    }
+ 
+    /**
+     * Add a menu item to the panel
+     *
+     * @method addItem
+     * @param {MenuItem|Object} item Menu item to add
+     * @returns {MenuPanel} This instance for chaining
+     */
+    addItem(item) {
+        if (item instanceof MenuItem) {
+            this.items.push(item.toObject());
+        } else {
+            this.items.push(item);
+        }
+        return this;
+    }
+ 
+    /**
+     * Add multiple menu items to the panel
+     *
+     * @method addItems
+     * @param {Array} items Array of menu items
+     * @returns {MenuPanel} This instance for chaining
+     */
+    addItems(items) {
+        items.forEach((item) => this.addItem(item));
+        return this;
+    }
+ 
+    /**
+     * Get the plain object representation
+     *
+     * @method toObject
+     * @returns {Object} Plain object with all panel properties
+     */
+    toObject() {
+        return {
+            title: this.title,
+            slug: this.slug,
+            icon: this.icon,
+            open: this.open,
+            priority: this.priority,
+            items: this.items,
+            // Indicator flag
+            _isMenuPanel: true,
+            ...this._options,
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/template-helper.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/template-helper.js.html new file mode 100644 index 0000000..bc38fe2 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/template-helper.js.html @@ -0,0 +1,367 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/template-helper.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts template-helper.js

+
+ +
+ 0% + Statements + 0/18 +
+ + +
+ 0% + Branches + 0/4 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/18 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Represents a helper that can be lazy-loaded from an engine or registered directly.
+ * Used by the RegistryService to share helpers across engines.
+ *
+ * @class TemplateHelper
+ * @example
+ * // Lazy loading from engine
+ * new TemplateHelper('@fleetbase/storefront-engine', 'helpers/calculate-delivery-fee')
+ *
+ * // Direct class registration
+ * new TemplateHelper(null, CalculateDeliveryFeeHelper)
+ */
+export default class TemplateHelper {
+    /**
+     * The name of the engine this helper belongs to
+     * @type {string|null}
+     */
+    engineName = null;
+ 
+    /**
+     * The path to the helper within the engine (for lazy loading)
+     * @type {string|null}
+     */
+    path = null;
+ 
+    /**
+     * The helper class or function (for direct registration)
+     * @type {Function|null}
+     */
+    class = null;
+ 
+    /**
+     * Whether this is a direct class registration (true) or lazy loading (false)
+     * @type {boolean}
+     */
+    isClass = false;
+ 
+    /**
+     * The resolved helper name (extracted from path or class name)
+     * @type {string|null}
+     */
+    name = null;
+ 
+    /**
+     * Creates a new TemplateHelper instance
+     *
+     * @param {string|null} engineName - The engine name (e.g., '@fleetbase/storefront-engine') or null for direct class
+     * @param {string|Function} pathOrClass - Either a path string for lazy loading or a helper class/function
+     */
+    constructor(engineName, pathOrClass) {
+        this.engineName = engineName;
+ 
+        if (typeof pathOrClass === 'string') {
+            // Lazy loading case
+            this.path = pathOrClass;
+            this.isClass = false;
+            this.name = this.#extractNameFromPath(pathOrClass);
+        } else {
+            // Direct class/function registration
+            this.class = pathOrClass;
+            this.isClass = true;
+            this.name = this.#extractNameFromClass(pathOrClass);
+        }
+    }
+ 
+    /**
+     * Extracts helper name from path
+     * @private
+     * @param {string} path - The helper path (e.g., 'helpers/calculate-delivery-fee')
+     * @returns {string} The helper name (e.g., 'calculate-delivery-fee')
+     */
+    #extractNameFromPath(path) {
+        const parts = path.split('/');
+        return parts[parts.length - 1];
+    }
+ 
+    /**
+     * Extracts helper name from class/function
+     * @private
+     * @param {Function} classOrFn - The helper class or function
+     * @returns {string|null} The helper name or null if not available
+     */
+    #extractNameFromClass(classOrFn) {
+        if (classOrFn.name) {
+            // Convert PascalCase or camelCase to kebab-case
+            return classOrFn.name
+                .replace(/([a-z])([A-Z])/g, '$1-$2')
+                .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
+                .toLowerCase()
+                .replace(/helper$/, ''); // Remove 'helper' suffix if present
+        }
+        return null;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/universe-registry.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/universe-registry.js.html new file mode 100644 index 0000000..0d9991c --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/universe-registry.js.html @@ -0,0 +1,157 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/universe-registry.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts universe-registry.js

+
+ +
+ 0% + Statements + 0/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 0% + Lines + 0/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { tracked } from '@glimmer/tracking';
+import { TrackedMap } from 'tracked-built-ins';
+ 
+/**
+ * UniverseRegistry
+ *
+ * A singleton registry class that stores all universe registrations.
+ * This class is registered to the application container to ensure
+ * the same registry instance is shared across the app and all engines.
+ *
+ * Pattern inspired by RouteOptimizationRegistry - ensures registrations
+ * persist across engine boundaries by storing data in the application
+ * container rather than in service instances.
+ *
+ * @class UniverseRegistry
+ */
+export default class UniverseRegistry {
+    /**
+     * TrackedMap of section name → TrackedObject with dynamic lists
+     * Fully reactive - templates update when registries change
+     * @type {TrackedMap<string, TrackedObject>}
+     */
+    @tracked registries = new TrackedMap();
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/widget.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/widget.js.html new file mode 100644 index 0000000..4065c3b --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/widget.js.html @@ -0,0 +1,910 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts/widget.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/contracts widget.js

+
+ +
+ 0% + Statements + 0/61 +
+ + +
+ 0% + Branches + 0/30 +
+ + +
+ 0% + Functions + 0/14 +
+ + +
+ 0% + Lines + 0/61 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import BaseContract from './base-contract';
+import ExtensionComponent from './extension-component';
+import isObject from '../utils/is-object';
+ 
+/**
+ * Represents a dashboard widget
+ *
+ * Widgets are modular components that can be added to dashboards.
+ * They support grid layout options, custom configurations, and lazy-loaded components.
+ *
+ * @class Widget
+ * @extends BaseContract
+ *
+ * @example
+ * // With chaining
+ * new Widget('fleet-ops-metrics')
+ *   .withName('Fleet-Ops Metrics')
+ *   .withDescription('Key metrics from Fleet-Ops')
+ *   .withIcon('truck')
+ *   .withComponent(new ExtensionComponent('@fleetbase/fleetops-engine', 'components/widget/metrics'))
+ *   .withGridOptions({ w: 12, h: 12, minW: 8, minH: 12 })
+ *   .asDefault()
+ *
+ * @example
+ * // Full definition object (first-class)
+ * new Widget({
+ *   id: 'fleet-ops-metrics',
+ *   name: 'Fleet-Ops Metrics',
+ *   description: 'Key metrics from Fleet-Ops',
+ *   icon: 'truck',
+ *   component: new ExtensionComponent('@fleetbase/fleetops-engine', 'components/widget/metrics'),
+ *   grid_options: { w: 12, h: 12, minW: 8, minH: 12 },
+ *   default: true
+ * })
+ *
+ * @example
+ * // Full definition with string component (local)
+ * new Widget({
+ *   id: 'welcome',
+ *   name: 'Welcome',
+ *   component: 'widget/welcome',
+ *   default: true
+ * })
+ */
+export default class Widget extends BaseContract {
+    /**
+     * Create a new Widget
+     *
+     * @constructor
+     * @param {String|Object} idOrDefinition Unique widget identifier or full definition object
+     */
+    constructor(idOrDefinition) {
+        // Call super FIRST (JavaScript requirement)
+        super(isObject(idOrDefinition) ? idOrDefinition : { id: idOrDefinition });
+ 
+        // THEN set properties
+        if (isObject(idOrDefinition)) {
+            const definition = idOrDefinition;
+ 
+            // Support both id and widgetId for backward compatibility
+            this.id = definition.id || definition.widgetId;
+            this.name = definition.name || null;
+            this.description = definition.description || null;
+            this.icon = definition.icon || null;
+            this.grid_options = definition.grid_options || {};
+            this.options = definition.options || {};
+            this.category = definition.category || 'default';
+ 
+            // Handle component - support both string and ExtensionComponent
+            if (definition.component instanceof ExtensionComponent) {
+                this.component = definition.component.toObject();
+            } else if (isObject(definition.component)) {
+                // Plain object component definition
+                this.component = definition.component;
+            } else {
+                // String component path
+                this.component = definition.component || null;
+            }
+ 
+            // Store default flag if present
+            if (definition.default) {
+                this._options.default = true;
+            }
+        } else {
+            // Handle string id (chaining pattern)
+            this.id = idOrDefinition;
+            this.name = null;
+            this.description = null;
+            this.icon = null;
+            this.component = null;
+            this.grid_options = {};
+            this.options = {};
+            this.category = 'default';
+        }
+ 
+        // Call setup() to trigger validation after properties are set
+        super.setup();
+    }
+ 
+    /**
+     * Validate the widget
+     *
+     * @method validate
+     * @throws {Error} If id is missing
+     */
+    validate() {
+        if (!this.id) {
+            throw new Error('Widget requires an id');
+        }
+    }
+ 
+    /**
+     * Set the widget name
+     *
+     * @method withName
+     * @param {String} name Display name
+     * @returns {Widget} This instance for chaining
+     */
+    withName(name) {
+        this.name = name;
+        this._options.name = name;
+        return this;
+    }
+ 
+    /**
+     * Set the widget description
+     *
+     * @method withDescription
+     * @param {String} description Widget description
+     * @returns {Widget} This instance for chaining
+     */
+    withDescription(description) {
+        this.description = description;
+        this._options.description = description;
+        return this;
+    }
+ 
+    /**
+     * Set the widget icon
+     *
+     * @method withIcon
+     * @param {String} icon Icon name
+     * @returns {Widget} This instance for chaining
+     */
+    withIcon(icon) {
+        this.icon = icon;
+        this._options.icon = icon;
+        return this;
+    }
+ 
+    /**
+     * Set the widget component
+     * Supports both string paths and ExtensionComponent instances
+     *
+     * @method withComponent
+     * @param {String|ExtensionComponent|Object} component Component definition
+     * @returns {Widget} This instance for chaining
+     */
+    withComponent(component) {
+        if (component instanceof ExtensionComponent) {
+            this.component = component.toObject();
+        } else {
+            this.component = component;
+        }
+        this._options.component = this.component;
+        return this;
+    }
+ 
+    /**
+     * Set grid layout options
+     *
+     * @method withGridOptions
+     * @param {Object} options Grid options (w, h, minW, minH, etc.)
+     * @returns {Widget} This instance for chaining
+     */
+    withGridOptions(options) {
+        this.grid_options = { ...this.grid_options, ...options };
+        this._options.grid_options = this.grid_options;
+        return this;
+    }
+ 
+    /**
+     * Set widget-specific options
+     *
+     * @method withOptions
+     * @param {Object} options Widget options
+     * @returns {Widget} This instance for chaining
+     */
+    withOptions(options) {
+        this.options = { ...this.options, ...options };
+        this._options.options = this.options;
+        return this;
+    }
+ 
+    /**
+     * Set the widget category
+     *
+     * @method withCategory
+     * @param {String} category Category name
+     * @returns {Widget} This instance for chaining
+     */
+    withCategory(category) {
+        this.category = category;
+        this._options.category = category;
+        return this;
+    }
+ 
+    /**
+     * Mark this widget as a default widget
+     * Default widgets are automatically added to new dashboards
+     *
+     * @method asDefault
+     * @returns {Widget} This instance for chaining
+     */
+    asDefault() {
+        this._options.default = true;
+        return this;
+    }
+ 
+    /**
+     * Check if this widget is marked as default
+     *
+     * @method isDefault
+     * @returns {Boolean} True if widget is a default widget
+     */
+    isDefault() {
+        return this._options.default === true;
+    }
+ 
+    /**
+     * Set the widget title
+     *
+     * @method withTitle
+     * @param {String} title Widget title
+     * @returns {Widget} This instance for chaining
+     */
+    withTitle(title) {
+        this.options.title = title;
+        this._options.options = this.options;
+        return this;
+    }
+ 
+    /**
+     * Set refresh interval for the widget
+     *
+     * @method withRefreshInterval
+     * @param {Number} milliseconds Refresh interval in milliseconds
+     * @returns {Widget} This instance for chaining
+     */
+    withRefreshInterval(milliseconds) {
+        this.options.refreshInterval = milliseconds;
+        this._options.options = this.options;
+        return this;
+    }
+ 
+    /**
+     * Get the plain object representation
+     *
+     * @method toObject
+     * @returns {Object} Plain object with all widget properties
+     */
+    toObject() {
+        return {
+            id: this.id,
+            name: this.name,
+            description: this.description,
+            icon: this.icon,
+            component: this.component,
+            grid_options: this.grid_options,
+            options: this.options,
+            category: this.category,
+            ...this._options,
+        };
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers/index.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers/index.html new file mode 100644 index 0000000..fe8c013 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers + + + + + + + + + +
+
+

All files ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers

+
+ +
+ 77.77% + Statements + 7/9 +
+ + +
+ 50% + Branches + 2/4 +
+ + +
+ 50% + Functions + 2/4 +
+ + +
+ 77.77% + Lines + 7/9 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
load-socketcluster-client.js +
+
100%5/550%1/2100%1/1100%5/5
local-storage-adapter.js +
+
50%2/450%1/233.33%1/350%2/4
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers/load-socketcluster-client.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers/load-socketcluster-client.js.html new file mode 100644 index 0000000..08db821 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers/load-socketcluster-client.js.html @@ -0,0 +1,127 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers/load-socketcluster-client.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers load-socketcluster-client.js

+
+ +
+ 100% + Statements + 5/5 +
+ + +
+ 50% + Branches + 1/2 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 5/5 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15  +  +  +1x +1x +1x +1x +1x +  +  +  +  +  +  + 
export function initialize() {
+    // Check if the script already exists
+    // Only insert the script tag if it doesn't already exist
+    Eif (!document.querySelector('script[data-socketcluster-client]')) {
+        const socketClusterClientScript = document.createElement('script');
+        socketClusterClientScript.setAttribute('data-socketcluster-client', '1');
+        socketClusterClientScript.src = '/assets/socketcluster-client.min.js';
+        document.body.appendChild(socketClusterClientScript);
+    }
+}
+ 
+export default {
+    initialize,
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers/local-storage-adapter.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers/local-storage-adapter.js.html new file mode 100644 index 0000000..b20e6a4 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers/local-storage-adapter.js.html @@ -0,0 +1,148 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers/local-storage-adapter.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/initializers local-storage-adapter.js

+
+ +
+ 50% + Statements + 2/4 +
+ + +
+ 50% + Branches + 1/2 +
+ + +
+ 33.33% + Functions + 1/3 +
+ + +
+ 50% + Lines + 2/4 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22  +  +  +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import Store from '@ember-data/store';
+import { importData, exportData } from 'ember-local-storage/helpers/import-export';
+ 
+export function initialize() {
+    Eif (!Store.prototype._emberLocalStoragePatched) {
+        Store.reopen({
+            _emberLocalStoragePatched: true,
+            importData: function (json, options) {
+                return importData(this, json, options);
+            },
+            exportData: function (types, options) {
+                return exportData(this, types, options);
+            },
+        });
+    }
+}
+ 
+export default {
+    name: 'local-storage-adapter',
+    initialize: initialize,
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/serializers/application.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/serializers/application.js.html new file mode 100644 index 0000000..585cc29 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/serializers/application.js.html @@ -0,0 +1,331 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/serializers/application.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/serializers application.js

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 80% + Branches + 4/5 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 18/18 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83  +  +  +  +  +  +353x +  +  +  +  +  +  +353x +  +  +  +  +  +  +  +  +14x +  +  +  +  +  +  +  +  +  +  +  +94x +  +94x +  +  +94x +380x +  +380x +216x +  +  +  +  +216x +188x +  +  +28x +  +  +  +94x +  +  +  +94x +94x +  +  +94x +  +  +  +  +  +  +  +  +  +  +  +  +161x +  +161x +  +  + 
import RESTSerializer from '@ember-data/serializer/rest';
+import { isNone } from '@ember/utils';
+import { underscore } from '@ember/string';
+import normalizePolymorphicTypeWithinHash from '../utils/serialize/normalize-polymorphic-type-within-hash';
+ 
+export default class ApplicationSerializer extends RESTSerializer {
+    readOnlyAttributes = ['slug'];
+ 
+    /**
+     * Default primary keys to uuid
+     *
+     * @var {string}
+     */
+    primaryKey = 'uuid';
+ 
+    /**
+     * keyForPolymorphicType can be used to define a custom key when serializing and deserializing a polymorphic type.
+     * By default, the returned key is ${key}Type.
+     *
+     * @var {String}
+     */
+    keyForPolymorphicType(key) {
+        return `${underscore(key)}_type`;
+    }
+ 
+    /**
+     * Customize serializer so that any attributes that are instances of Models or objects
+     * that are to accept and ID get serialized into the id only
+     *
+     * @param {Snapshot} snapshot
+     * @param {Object} options
+     * @return {Object} json
+     */
+    serialize(snapshot) {
+        const json = super.serialize(...arguments);
+ 
+        this.removeReadOnlyAttributes(json);
+ 
+        // for each relationship make sure the id is set
+        snapshot.eachRelationship((key, relationship) => {
+            const { kind } = relationship.meta;
+ 
+            if (kind === 'belongsTo') {
+                const relationSnapshot = snapshot.belongsTo(key);
+ 
+                /* istanbul ignore next -- keyForRelationship is defined on the serializer prototype, so the fallback arm is unreachable */
+                key = this.keyForRelationship ? this.keyForRelationship(key, 'belongsTo', 'serialize') : key;
+ 
+                if (isNone(relationSnapshot)) {
+                    return;
+                }
+ 
+                json[`${key}_uuid`] = relationSnapshot.id;
+            }
+        });
+ 
+        return json;
+    }
+ 
+    removeReadOnlyAttributes(json = {}) {
+        this.readOnlyAttributes.forEach((attribute) => {
+            delete json[attribute];
+        });
+ 
+        return json;
+    }
+ 
+    /**
+     * Normalizes a part of the JSON payload returned by the server.
+     *
+     * @method normalize
+     * @param {Model} modelClass
+     * @param {Object} resourceHash
+     * @param {String} prop
+     * @return {Object}
+     */
+    normalize(model, hash, prop) {
+        hash = normalizePolymorphicTypeWithinHash(hash);
+ 
+        return super.normalize(model, hash, prop);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/serializers/index.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/serializers/index.html new file mode 100644 index 0000000..3ceea81 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/serializers/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/serializers + + + + + + + + + +
+
+

All files ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/serializers

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 80% + Branches + 4/5 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 18/18 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
application.js +
+
100%18/1880%4/5100%6/6100%18/18
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/current-user.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/current-user.js.html new file mode 100644 index 0000000..b530d77 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/current-user.js.html @@ -0,0 +1,1447 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/current-user.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services current-user.js

+
+ +
+ 0% + Statements + 0/150 +
+ + +
+ 0% + Branches + 0/66 +
+ + +
+ 0% + Functions + 0/29 +
+ + +
+ 0% + Lines + 0/148 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import Service from '@ember/service';
+import Evented from '@ember/object/evented';
+import { inject as service } from '@ember/service';
+import { tracked } from '@glimmer/tracking';
+import { dasherize } from '@ember/string';
+import { get } from '@ember/object';
+import { isBlank } from '@ember/utils';
+import { alias } from '@ember/object/computed';
+import { storageFor } from 'ember-local-storage';
+import { debug } from '@ember/debug';
+import lookupUserIp, { getBrowserTimezone } from '../utils/lookup-user-ip';
+ 
+/**
+ * CurrentUserService
+ *
+ * Manages the authenticated user's identity and preferences. Extends Evented
+ * so that any service or component can subscribe to user lifecycle events
+ * directly on this service.
+ *
+ * Session lifecycle events emitted (on both this service and via EventsService
+ * which re-broadcasts them on the universe bus for cross-engine listeners):
+ *
+ *   user.loaded          — fired after a successful login or session restore.
+ *                          Payload: (user, organization, properties)
+ *
+ *   user.updated         — fired when the user record is refreshed in-session
+ *                          (e.g. profile edit). Payload: (user, properties)
+ *
+ *   user.organization_switched — fired when the user switches active org.
+ *                          Payload: (organization, properties)
+ */
+export default class CurrentUserService extends Service.extend(Evented) {
+    @service session;
+    @service store;
+    @service fetch;
+    @service theme;
+    @service notifications;
+    @service intl;
+    @service events;
+    @service universe;
+ 
+    @tracked user = { id: 'anon' };
+    @tracked userSnapshot = { id: 'anon' };
+    @tracked company = {};
+    @tracked permissions = [];
+    @tracked organizations = [];
+    @tracked whoisData = {};
+    @tracked locale = 'en-us';
+ 
+    @storageFor('user-options') options;
+    @storageFor('local-cache') cache;
+    @alias('userSnapshot.id') id;
+    @alias('userSnapshot.name') name;
+    @alias('userSnapshot.phone') phone;
+    @alias('userSnapshot.email') email;
+    @alias('userSnapshot.avatar_url') avatarUrl;
+    @alias('userSnapshot.is_admin') isAdmin;
+    @alias('userSnapshot.company_uuid') companyId;
+    @alias('userSnapshot.company_name') companyName;
+    @alias('userSnapshot.role_name') roleName;
+    @alias('userSnapshot.role') role;
+ 
+    get authenticatedOptionOwnerId() {
+        const authenticatedUserId = this.session?.data?.authenticated?.user;
+ 
+        if (this.session?.isAuthenticated && authenticatedUserId) {
+            return authenticatedUserId;
+        }
+ 
+        try {
+            const localStorageSession = JSON.parse(window.localStorage.getItem('ember_simple_auth-session'));
+            const authenticatedSession = localStorageSession?.authenticated;
+ 
+            if (authenticatedSession?.token && authenticatedSession?.user) {
+                return authenticatedSession.user;
+            }
+        } catch (error) {
+            // Ignore malformed session storage and fall back to the current snapshot.
+        }
+ 
+        return null;
+    }
+ 
+    get optionsPrefix() {
+        return `${this.authenticatedOptionOwnerId || this.id || 'anon'}:`;
+    }
+ 
+    get latitude() {
+        return this.whois('latitude');
+    }
+ 
+    get longitude() {
+        return this.whois('longitude');
+    }
+ 
+    get currency() {
+        return this.whois('currency.code');
+    }
+ 
+    get city() {
+        return this.whois('city');
+    }
+ 
+    get country() {
+        return this.whois('country_code');
+    }
+ 
+    get timezone() {
+        return this.whois('timezone') || getBrowserTimezone();
+    }
+ 
+    async load() {
+        if (this.session.isAuthenticated) {
+            const user = await this.store.findRecord('user', 'me');
+ 
+            // set user
+            await this.setUser(user);
+ 
+            // Load preferences
+            await this.loadPreferences();
+ 
+            return user;
+        }
+ 
+        return null;
+    }
+ 
+    async promiseUser(options = {}) {
+        const NoUserAuthenticatedError = new Error('Failed to authenticate user.');
+        if (!this.session.isAuthenticated) {
+            throw NoUserAuthenticatedError;
+        }
+ 
+        try {
+            const user = await this.store.queryRecord('user', { me: true });
+ 
+            // set user
+            await this.setUser(user);
+ 
+            // Load user whois data
+            await this.loadWhois();
+ 
+            // Load user organizations
+            await this.loadOrganizations();
+ 
+            // Optional callback
+            if (typeof options?.onUserResolved === 'function') {
+                options.onUserResolved(user);
+            }
+ 
+            return user;
+        } catch (error) {
+            debug(`Error loading current user : ${error.message}`);
+            throw error;
+        }
+    }
+ 
+    async loadPreferences() {
+        await this.loadLocale();
+        await this.loadWhois();
+        await this.loadOrganizations();
+    }
+ 
+    async loadLocale() {
+        try {
+            const { locale } = await this.fetch.get('users/locale');
+            this.setLocale(locale);
+ 
+            return locale;
+        } catch (error) {
+            this.notifications.serverError(error);
+        }
+    }
+ 
+    async loadOrganizations() {
+        try {
+            const organizations = await this.fetch.get('auth/organizations', {}, { normalizeToEmberData: true, normalizeModelType: 'company' });
+            this.setOption('organizations', organizations);
+            this.organizations = organizations;
+ 
+            return organizations;
+        } catch (error) {
+            this.notifications.serverError(error);
+        }
+    }
+ 
+    async loadWhois() {
+        // Use frontend IP lookup to get accurate user location
+        // This avoids the issue of server-side lookup returning server IP instead of user IP
+        const whois = await lookupUserIp({
+            timeout: 5000,
+            cache: true,
+        });
+ 
+        // lookupUserIp absorbs its own failures and hands back a fallback rather than
+        // rejecting, so the result, not an exception, says the location is unknown.
+        if (whois._source === 'fallback') {
+            this.notifications.warning('Unable to detect your location. Some features may use default settings.');
+        }
+ 
+        this.whoisData = whois;
+        try {
+            this.setOption('whois', whois);
+        } catch (error) {
+            console.error('[currentUser] Failed to store whois:', error);
+        }
+ 
+        return whois;
+    }
+ 
+    getCompany() {
+        this.company = this.store.peekRecord('company', this.user.company_uuid);
+        return this.company;
+    }
+ 
+    async loadCompany() {
+        const company = this.store.peekRecord('company', this.user.company_uuid);
+        if (company) {
+            return company;
+        }
+ 
+        return this.store.findRecord('company', this.user.company_uuid);
+    }
+ 
+    getUserPermissions(user) {
+        const permissions = [];
+ 
+        // get direct applied permissions
+        if (user.get('permissions')) {
+            permissions.push(...user.get('permissions').toArray());
+        }
+ 
+        // get role permissions and role policies permissions
+        if (user.get('role')) {
+            if (user.get('role.permissions')) {
+                permissions.push(...user.get('role.permissions').toArray());
+            }
+ 
+            if (user.get('role.policies')) {
+                for (let i = 0; i < user.get('role.policies').length; i++) {
+                    const policy = user.get('role.policies').objectAt(i);
+                    if (policy.get('permissions')) {
+                        permissions.push(...policy.get('permissions').toArray());
+                    }
+                }
+            }
+        }
+ 
+        // get direct applied policy permissions
+        if (user.get('policies')) {
+            for (let i = 0; i < user.get('policies').length; i++) {
+                const policy = user.get('policies').objectAt(i);
+                if (policy.get('permissions')) {
+                    permissions.push(...policy.get('permissions').toArray());
+                }
+            }
+        }
+ 
+        return permissions;
+    }
+ 
+    whois(key) {
+        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)}`;
+ 
+        this.options.set(key, value);
+ 
+        return this;
+    }
+ 
+    getOption(key, defaultValue = null) {
+        key = `${this.optionsPrefix}${dasherize(key)}`;
+ 
+        const value = this.options.get(key);
+        return value !== undefined ? value : defaultValue;
+    }
+ 
+    getWhoisProperty(prop) {
+        const whois = this.getOption('whois');
+ 
+        if (!whois || typeof whois !== 'object') {
+            // Fallback to lookup/whois in local-cache
+            const cachedWhois = this.cache.get('lookup/whois');
+            if (cachedWhois) {
+                return get(cachedWhois, prop);
+            }
+            return null;
+        }
+ 
+        return get(whois, prop);
+    }
+ 
+    hasOption(key) {
+        // Read storage directly rather than going through getOption: its
+        // `defaultValue = null` parameter applies whenever the stored value is
+        // undefined, so getOption can never return undefined and this was
+        // always true.
+        key = `${this.optionsPrefix}${dasherize(key)}`;
+ 
+        return this.options.get(key) !== undefined;
+    }
+ 
+    filledOption(key) {
+        return !isBlank(this.getOption(key));
+    }
+ 
+    async getUserSnapshot(user) {
+        const role = await user.get('role');
+        const snapshot = user.serialize({ includeId: true });
+ 
+        return {
+            ...snapshot,
+            id: snapshot.uuid,
+            company_name: user.get('company_name'),
+            role_name: user.get('role_name'),
+            role: {
+                ...role.serialize({ includeId: true }),
+                id: role.get('id'),
+            },
+        };
+    }
+ 
+    getCompanyOption(key, defaultValue = null) {
+        const company = this.store.peekRecord('company', this.companyId);
+        if (company) {
+            const value = get(company, `options.${key}`);
+            if (value === undefined) {
+                return defaultValue;
+            }
+ 
+            return value;
+        }
+ 
+        return defaultValue;
+    }
+ 
+    /**
+     * Sets the current user and fires all user.loaded lifecycle events.
+     *
+     * This is the canonical place where the authenticated user identity is
+     * established. It fires:
+     *
+     *   1. `this.trigger('user.loaded', user)` — on the currentUser service
+     *      itself (Evented), for direct service-level listeners.
+     *
+     *   2. `this.events.trackUserLoaded(user, organization)` — on the events
+     *      service, which re-broadcasts on both the events bus and the universe
+     *      bus so cross-engine listeners (Intercom, PostHog, Attio, etc.) can
+     *      subscribe via `universe.on('user.loaded', handler)`.
+     *
+     * @param {Model} user
+     */
+    async setUser(user) {
+        const snapshot = await this.getUserSnapshot(user);
+ 
+        // Set current user
+        this.set('user', user);
+        this.set('userSnapshot', snapshot);
+        this.theme.syncThemeFromCurrentUser();
+ 
+        // Resolve the organization for event payload
+        const organization = this.store.peekRecord('company', user.get('company_uuid'));
+ 
+        // 1. Trigger on the currentUser Evented bus (backward-compatible)
+        this.trigger('user.loaded', user);
+ 
+        // 2. Fire through the events service — broadcasts on both events bus
+        //    and universe bus for cross-engine listeners
+        if (this.events) {
+            this.events.trackUserLoaded(user, organization);
+        }
+ 
+        // 3. Trigger directly on universe for framework-level uniformity —
+        //    guarantees delivery to all engines on the shared bus
+        if (this.universe) {
+            this.universe.trigger('user.loaded', user, organization);
+        }
+ 
+        // Set permissions
+        this.permissions = this.getUserPermissions(user);
+ 
+        // Set environment from user option
+        this.theme.setEnvironment();
+ 
+        // Set locale
+        if (user.locale) {
+            this.setLocale(user.locale);
+        } else {
+            await this.loadLocale();
+        }
+    }
+ 
+    /**
+     * Fires a user.updated event when the user record is refreshed in-session.
+     * Call this after any in-session profile update to keep integrations in sync.
+     *
+     * @param {Model} user
+     */
+    async refreshUser(user) {
+        const snapshot = await this.getUserSnapshot(user);
+        this.set('user', user);
+        this.set('userSnapshot', snapshot);
+ 
+        const organization = this.store.peekRecord('company', user.get('company_uuid'));
+ 
+        this.trigger('user.updated', user);
+ 
+        if (this.events) {
+            this.events.trackEvent('user.updated', {
+                user_id: user?.id,
+                organization_id: organization?.id,
+                organization_name: organization?.name,
+            });
+        }
+ 
+        if (this.universe) {
+            this.universe.trigger('user.updated', user, organization);
+        }
+    }
+ 
+    /**
+     * Fires a user.organization_switched event when the user changes their
+     * active organization. Call this after a successful org switch.
+     *
+     * @param {Model} organization
+     */
+    switchOrganization(organization) {
+        this.company = organization;
+ 
+        this.trigger('user.organization_switched', organization);
+ 
+        if (this.events) {
+            this.events.trackEvent('user.organization_switched', {
+                organization_id: organization?.id,
+                organization_name: organization?.name,
+            });
+        }
+ 
+        if (this.universe) {
+            this.universe.trigger('user.organization_switched', organization);
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/events.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/events.js.html new file mode 100644 index 0000000..89db37a --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/events.js.html @@ -0,0 +1,1435 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/events.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services events.js

+
+ +
+ 0% + Statements + 0/75 +
+ + +
+ 0% + Branches + 0/57 +
+ + +
+ 0% + Functions + 0/17 +
+ + +
+ 0% + Lines + 0/75 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import Service, { inject as service } from '@ember/service';
+import Evented from '@ember/object/evented';
+import config from 'ember-get-config';
+ 
+/**
+ * EventsService
+ *
+ * Provides a centralized, standardized event tracking system for Fleetbase.
+ *
+ * This service is the single source of truth for all application lifecycle
+ * events. It emits every event on two buses simultaneously:
+ *
+ *   1. Its own Evented bus — for direct service/component listeners:
+ *        this.events.on('user.loaded', handler)
+ *
+ *   2. The universe service bus — for cross-engine listeners (recommended
+ *      for use in Ember Engines / extensions):
+ *        this.universe.on('user.loaded', handler)
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ * Session Lifecycle Events
+ * ─────────────────────────────────────────────────────────────────────────────
+ *
+ *   session.authenticated    Fired after a successful login or session restore.
+ *                            Payload: (properties)
+ *
+ *   session.invalidated      Fired after the session is destroyed (logout).
+ *                            Payload: (duration_seconds, properties)
+ *
+ *   session.terminated       Alias for session.invalidated — provided for
+ *                            backward compatibility and semantic clarity.
+ *                            Payload: (duration_seconds, properties)
+ *
+ *   user.loaded              Fired after the authenticated user record and
+ *                            organization have been fully loaded into the
+ *                            currentUser service. This is the canonical event
+ *                            for integrations to boot (Intercom, PostHog, etc.)
+ *                            Payload: (user, organization, properties)
+ *
+ *   user.updated             Fired when the user record is refreshed in-session
+ *                            (e.g. after a profile edit).
+ *                            Payload: (user, properties)
+ *
+ *   user.deauthenticated     Fired when the user identity is cleared on logout.
+ *                            Semantic alias for session.invalidated — use this
+ *                            to shut down integrations cleanly (Intercom, etc.)
+ *                            Payload: (duration_seconds, properties)
+ *
+ *   user.organization_switched  Fired when the user switches their active org.
+ *                            Payload: (organization, properties)
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ * Resource Events
+ * ─────────────────────────────────────────────────────────────────────────────
+ *
+ *   resource.created         Generic resource creation.
+ *   resource.updated         Generic resource update.
+ *   resource.deleted         Generic resource deletion.
+ *   resource.imported        Bulk import.
+ *   resource.exported        Export.
+ *   resource.bulk_action     Bulk action (delete, archive, etc.)
+ *   {modelName}.created      Model-specific creation (e.g. order.created)
+ *   {modelName}.updated      Model-specific update.
+ *   {modelName}.deleted      Model-specific deletion.
+ *   {modelName}.exported     Model-specific export.
+ *
+ * @class EventsService
+ * @extends Service
+ */
+export default class EventsService extends Service.extend(Evented) {
+    @service universe;
+    @service currentUser;
+ 
+    // =========================================================================
+    // Session Lifecycle Tracking
+    // =========================================================================
+ 
+    /**
+     * Tracks a successful authentication (login or session restore).
+     *
+     * Called by SessionService.handleAuthentication().
+     *
+     * @param {Object} [props={}] - Additional properties to include
+     */
+    trackSessionAuthenticated(props = {}) {
+        const properties = this.#enrichProperties(props);
+        this.#trigger('session.authenticated', properties);
+    }
+ 
+    /**
+     * Tracks when a user session is terminated (logout).
+     *
+     * Called by SessionService.handleInvalidation(). Also fires the semantic
+     * `user.deauthenticated` event so integrations can react to the user
+     * identity being cleared without needing to know about session internals.
+     *
+     * @param {Number|null} duration - Session duration in seconds (null if unknown)
+     * @param {Object} [props={}] - Additional properties to include
+     */
+    trackSessionTerminated(duration, props = {}) {
+        const properties = this.#enrichProperties({
+            session_duration: duration,
+            ...props,
+        });
+ 
+        // Fire session.invalidated (technical event)
+        this.#trigger('session.invalidated', duration, properties);
+ 
+        // Fire session.terminated (backward-compatible alias)
+        this.#trigger('session.terminated', duration, properties);
+ 
+        // Fire user.deauthenticated (semantic event for integrations)
+        this.#trigger('user.deauthenticated', duration, properties);
+    }
+ 
+    /**
+     * Tracks when the current user is loaded (session initialized).
+     *
+     * Called by CurrentUserService.setUser() after a successful login or
+     * session restore. This is the canonical event for integrations to boot.
+     *
+     * @param {Object} user         - The authenticated user model
+     * @param {Object} organization - The user's active organization model
+     * @param {Object} [props={}]   - Additional properties to include
+     */
+    trackUserLoaded(user, organization, props = {}) {
+        const properties = this.#enrichProperties({
+            user_id: user?.id,
+            organization_id: organization?.id,
+            organization_name: organization?.name,
+            ...props,
+        });
+ 
+        this.#trigger('user.loaded', user, organization, properties);
+    }
+ 
+    /**
+     * Tracks when the user record is refreshed in-session (e.g. profile edit).
+     *
+     * Called by CurrentUserService.refreshUser().
+     *
+     * @param {Object} user       - The updated user model
+     * @param {Object} [props={}] - Additional properties to include
+     */
+    trackUserUpdated(user, props = {}) {
+        const properties = this.#enrichProperties({
+            user_id: user?.id,
+            ...props,
+        });
+ 
+        this.#trigger('user.updated', user, properties);
+    }
+ 
+    /**
+     * Tracks when the user switches their active organization.
+     *
+     * Called by CurrentUserService.switchOrganization().
+     *
+     * @param {Object} organization - The new active organization model
+     * @param {Object} [props={}]   - Additional properties to include
+     */
+    trackOrganizationSwitched(organization, props = {}) {
+        const properties = this.#enrichProperties({
+            organization_id: organization?.id,
+            organization_name: organization?.name,
+            ...props,
+        });
+ 
+        this.#trigger('user.organization_switched', organization, properties);
+    }
+ 
+    // =========================================================================
+    // Resource Event Tracking
+    // =========================================================================
+ 
+    /**
+     * Tracks the creation of a resource
+     *
+     * @param {Object} resource - The created resource/model
+     * @param {Object} [props={}] - Additional properties to include
+     */
+    trackResourceCreated(resource, props = {}) {
+        const events = this.#getResourceEvents(resource, 'created');
+        const properties = this.#enrichProperties({
+            ...this.#getSafeProperties(resource),
+            ...props,
+        });
+ 
+        events.forEach((eventName) => {
+            this.#trigger(eventName, resource, properties);
+        });
+    }
+ 
+    /**
+     * Tracks the update of a resource
+     *
+     * @param {Object} resource - The updated resource/model
+     * @param {Object} [props={}] - Additional properties to include
+     */
+    trackResourceUpdated(resource, props = {}) {
+        const events = this.#getResourceEvents(resource, 'updated');
+        const properties = this.#enrichProperties({
+            ...this.#getSafeProperties(resource),
+            ...props,
+        });
+ 
+        events.forEach((eventName) => {
+            this.#trigger(eventName, resource, properties);
+        });
+    }
+ 
+    /**
+     * Tracks the deletion of a resource
+     *
+     * @param {Object} resource - The deleted resource/model
+     * @param {Object} [props={}] - Additional properties to include
+     */
+    trackResourceDeleted(resource, props = {}) {
+        const events = this.#getResourceEvents(resource, 'deleted');
+        const properties = this.#enrichProperties({
+            ...this.#getSafeProperties(resource),
+            ...props,
+        });
+ 
+        events.forEach((eventName) => {
+            this.#trigger(eventName, resource, properties);
+        });
+    }
+ 
+    /**
+     * Tracks a bulk import of resources
+     *
+     * @param {String} modelName - The name of the model being imported
+     * @param {Number} count - Number of resources imported
+     * @param {Object} [props={}] - Additional properties to include
+     */
+    trackResourceImported(modelName, count, props = {}) {
+        const properties = this.#enrichProperties({
+            model_name: modelName,
+            count: count,
+            ...props,
+        });
+ 
+        this.#trigger('resource.imported', modelName, count, properties);
+    }
+ 
+    /**
+     * Tracks a resource export
+     *
+     * @param {String} modelName - The name of the model being exported
+     * @param {String} format - Export format (csv, xlsx, etc.)
+     * @param {Object} [params={}] - Export parameters/filters
+     * @param {Object} [props={}] - Additional properties to include
+     */
+    trackResourceExported(modelName, format, params = {}, props = {}) {
+        const properties = this.#enrichProperties({
+            model_name: modelName,
+            export_format: format,
+            has_filters: !!(params && Object.keys(params).length > 0),
+            ...props,
+        });
+ 
+        this.#trigger('resource.exported', modelName, format, params, properties);
+ 
+        // Also trigger model-specific export event
+        const specificEvent = `${modelName}.exported`;
+        this.#trigger(specificEvent, modelName, format, params, properties);
+    }
+ 
+    /**
+     * Tracks a bulk action on multiple resources
+     *
+     * @param {String} verb - The action verb (delete, archive, etc.)
+     * @param {Array} resources - Array of selected resources
+     * @param {Object} [props={}] - Additional properties to include
+     */
+    trackBulkAction(verb, resources, props = {}) {
+        const firstResource = resources && resources.length > 0 ? resources[0] : null;
+        const modelName = this.#getModelName(firstResource);
+ 
+        const properties = this.#enrichProperties({
+            action: verb,
+            count: resources?.length || 0,
+            model_name: modelName,
+            ...props,
+        });
+ 
+        this.#trigger('resource.bulk_action', verb, resources, firstResource, properties);
+    }
+ 
+    // =========================================================================
+    // Generic Event Tracking
+    // =========================================================================
+ 
+    /**
+     * Tracks a generic custom event
+     *
+     * @param {String} eventName - The event name (dot notation)
+     * @param {Object} [props={}] - Event properties
+     */
+    trackEvent(eventName, props = {}) {
+        const properties = this.#enrichProperties(props);
+        this.#trigger(eventName, properties);
+    }
+ 
+    /**
+     * Checks if event tracking is enabled
+     *
+     * @returns {Boolean}
+     */
+    isEnabled() {
+        const eventsConfig = config?.events || {};
+        return eventsConfig.enabled !== false; // Enabled by default
+    }
+ 
+    // =========================================================================
+    // Private Methods (using # syntax)
+    // =========================================================================
+ 
+    /**
+     * Triggers an event on both the events service and universe service.
+     *
+     * This dual event system allows listeners to subscribe to events on either:
+     *   - this.events.on('event.name', handler)  — local listeners
+     *   - this.universe.on('event.name', handler) — cross-engine listeners
+     *
+     * @private
+     * @param {String} eventName - The event name
+     * @param {...*} args - Arguments to pass to event listeners
+     */
+    #trigger(eventName, ...args) {
+        if (!this.isEnabled()) {
+            return;
+        }
+ 
+        // Debug logging if enabled
+        if (config?.events?.debug) {
+            console.log(`[Events] ${eventName}`, args);
+        }
+ 
+        // Trigger on events service (local listeners)
+        this.trigger(eventName, ...args);
+ 
+        // Trigger on universe service (cross-engine listeners)
+        if (this.universe) {
+            this.universe.trigger(eventName, ...args);
+        } else {
+            console.warn('[Events] Universe service not available');
+        }
+    }
+ 
+    /**
+     * Generates both generic and specific event names for a resource action
+     *
+     * @private
+     * @param {Object} resource - The resource/model
+     * @param {String} action - The action (created, updated, deleted)
+     * @returns {Array<String>} Array of event names
+     */
+    #getResourceEvents(resource, action) {
+        const modelName = this.#getModelName(resource);
+        return [`resource.${action}`, `${modelName}.${action}`];
+    }
+ 
+    /**
+     * Extracts safe, serializable properties from a resource
+     *
+     * @private
+     * @param {Object} resource - The resource/model
+     * @returns {Object} Safe properties object
+     */
+    #getSafeProperties(resource) {
+        if (!resource) {
+            return {};
+        }
+ 
+        const props = {
+            id: resource.id,
+            model_name: this.#getModelName(resource),
+        };
+ 
+        // Add common properties if available
+        const commonProps = ['name', 'status', 'type', 'slug', 'public_id'];
+        commonProps.forEach((prop) => {
+            if (resource[prop] !== undefined && resource[prop] !== null) {
+                props[prop] = resource[prop];
+            }
+        });
+ 
+        return props;
+    }
+ 
+    /**
+     * Enriches properties with user, organization, and timestamp context
+     *
+     * @private
+     * @param {Object} props - Base properties
+     * @returns {Object} Enriched properties
+     */
+    #enrichProperties(props) {
+        const eventsConfig = config?.events || {};
+        const enrichConfig = eventsConfig.enrich || {};
+        const enriched = { ...props };
+ 
+        // Add user context if enabled
+        if (enrichConfig.user !== false && this.currentUser?.user) {
+            enriched.user_id = this.currentUser.user.id;
+        }
+ 
+        // Add organization context if enabled
+        if (enrichConfig.organization !== false && this.currentUser?.organization) {
+            enriched.organization_id = this.currentUser.organization.id;
+        }
+ 
+        // Add timestamp if enabled
+        if (enrichConfig.timestamp !== false) {
+            enriched.timestamp = new Date().toISOString();
+        }
+ 
+        return enriched;
+    }
+ 
+    /**
+     * Safely extracts the model name from a resource
+     *
+     * @private
+     * @param {Object} resource - The resource/model
+     * @returns {String} Model name or 'unknown'
+     */
+    #getModelName(resource) {
+        if (!resource) {
+            return 'unknown';
+        }
+ 
+        // Try multiple ways to get model name
+        if (resource.constructor?.modelName) {
+            return resource.constructor.modelName;
+        }
+ 
+        if (resource._internalModel?.modelName) {
+            return resource._internalModel.modelName;
+        }
+ 
+        if (resource.modelName) {
+            return resource.modelName;
+        }
+ 
+        return 'unknown';
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/fetch.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/fetch.js.html new file mode 100644 index 0000000..df2e690 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/fetch.js.html @@ -0,0 +1,2236 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/fetch.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services fetch.js

+
+ +
+ 0.47% + Statements + 1/212 +
+ + +
+ 0.53% + Branches + 1/187 +
+ + +
+ 0% + Functions + 0/46 +
+ + +
+ 0.48% + Lines + 1/206 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import Service from '@ember/service';
+import { tracked } from '@glimmer/tracking';
+import { inject as service } from '@ember/service';
+import { get, set } from '@ember/object';
+import { isBlank } from '@ember/utils';
+import { dasherize, underscore } from '@ember/string';
+import { isArray } from '@ember/array';
+import { singularize, pluralize } from 'ember-inflector';
+import { task } from 'ember-concurrency';
+import { storageFor } from 'ember-local-storage';
+import { add, isAfter, parseISO } from 'date-fns';
+import { decompress as decompressJson } from 'compress-json';
+import config from 'ember-get-config';
+import corslite from '../utils/corslite';
+import getMimeType from '../utils/get-mime-type';
+import download from '../utils/download';
+import getUserOptions from '../utils/get-user-options';
+import isEmptyObject from '../utils/is-empty-object';
+import isObject from '../utils/is-object';
+import fetch from 'fetch';
+ 
+/* istanbul ignore if -- runs at import; whichever of adapters/application or services/fetch loads first sets the host, so the other can never enter */
+if (isBlank(config.API.host)) {
+    config.API.host = `${window.location.protocol}//${window.location.hostname}`;
+}
+ 
+export default class FetchService extends Service {
+    /**
+     * Creates an instance of FetchService.
+     * @memberof FetchService
+     */
+    constructor() {
+        super(...arguments);
+ 
+        this.headers = this.getHeaders();
+        this.host = get(config, 'API.host');
+        this.namespace = get(config, 'API.namespace');
+    }
+ 
+    /**
+     * Mutable headers property.
+     *
+     * @var {Array}
+     */
+    @tracked headers;
+ 
+    /**
+     * Mutable namespace property.
+     *
+     * @var {String}
+     */
+    @tracked namespace;
+ 
+    /**
+     * Mutable host property.
+     *
+     * @var {String}
+     */
+    @tracked host;
+ 
+    /**
+     * Gets headers that should be sent with request.
+     *
+     * @return {Object}
+     */
+    getHeaders() {
+        const headers = {};
+        const isAuthenticated = this.session.isAuthenticated;
+        const userId = this.session.data.authenticated.user;
+        const userOptions = getUserOptions();
+        const isSandbox = get(userOptions, `${userId}:sandbox`) === true;
+        // See the note in adapters/application.js: `setOption` dasherizes, so
+        // the stored key is `<user>:test-key`, not `<user>:testKey`.
+        const testKey = get(userOptions, `${userId}:test-key`);
+ 
+        headers['Content-Type'] = 'application/json';
+ 
+        if (isAuthenticated) {
+            headers['Authorization'] = `Bearer ${this.session.data.authenticated.token}`;
+        }
+ 
+        if (isAuthenticated && isSandbox) {
+            headers['Access-Console-Sandbox'] = true;
+        }
+ 
+        if (isAuthenticated && !isBlank(testKey)) {
+            headers['Access-Console-Sandbox-Key'] = testKey;
+        }
+ 
+        return headers;
+    }
+ 
+    /**
+     * Updates headers property before making request.
+     *
+     * @return {FetchService}
+     * @memberof FetchService
+     */
+    refreshHeaders() {
+        this.headers = this.getHeaders();
+ 
+        return this;
+    }
+ 
+    /**
+     * Allows namespace to be set before making fetch request.
+     *
+     * @param {String} namespace
+     * @return {FetchService}
+     * @memberof FetchService
+     */
+    setNamespace(namespace) {
+        this.namespace = namespace;
+ 
+        return this;
+    }
+ 
+    /**
+     * Allows host to be set before making fetch request.
+     *
+     * @param {String} host
+     * @return {FetchService}
+     * @memberof FetchService
+     */
+    setHost(host) {
+        this.host = host;
+ 
+        return this;
+    }
+ 
+    /**
+     * Credentials
+     *
+     * @var {String}
+     */
+    credentials = 'include';
+ 
+    /**
+     * Inject the `store` service
+     *
+     * @var {Service}
+     */
+    @service store;
+ 
+    /**
+     * Inject the `session` service
+     *
+     * @var {Service}
+     */
+    @service session;
+ 
+    /**
+     * Inject the `currentUser` service
+     *
+     * @var {Service}
+     */
+    @service currentUser;
+ 
+    /**
+     * Inject the `notifications` service
+     *
+     * @var {Service}
+     */
+    @service notifications;
+ 
+    /**
+     * Local cache for some static requests
+     *
+     * @var StorageObject
+     */
+    @storageFor('local-cache') localCache;
+ 
+    /**
+     * Normalizes a model response from fetch to a ember data model
+     *
+     * @param  {Object} payload   A response from a network request
+     * @param  {String} modelType The type of model to be normalized too
+     *
+     * @return {Model}            An ember model
+     */
+    normalizeModel(payload, modelType = null) {
+        if (modelType === null) {
+            const modelTypeKeys = Object.keys(payload);
+            // `Object.keys` returns a plain array, which has no `firstObject`
+            // once prototype extensions are off — this silently yielded
+            // undefined, so the payload was returned unnormalized.
+            modelType = modelTypeKeys.length ? modelTypeKeys[0] : false;
+        }
+ 
+        if (typeof modelType !== 'string') {
+            return payload;
+        }
+ 
+        const type = dasherize(singularize(modelType));
+        const pluralizedModelType = pluralize(underscore(modelType));
+ 
+        if (isArray(payload)) {
+            return payload.map((instance) => this.store.push(this.store.normalize(type, instance)));
+        }
+ 
+        if (isArray(payload[pluralizedModelType])) {
+            return payload[pluralizedModelType].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)));
+        }
+ 
+        if (!isBlank(payload) && isBlank(payload[modelType])) {
+            return this.jsonToModel(payload, type);
+        }
+ 
+        return this.store.push(this.store.normalize(type, payload[modelType]));
+    }
+ 
+    /**
+     * Normalizes a model response from a JSON object or string
+     *
+     * @param  {Object} payload   A response from a network request
+     * @param  {String} modelType The type of model to be normalized too
+     *
+     * @return {Model}            An ember model
+     */
+    jsonToModel(attributes = {}, modelType) {
+        if (typeof attributes === 'string') {
+            attributes = JSON.parse(attributes);
+        }
+ 
+        const type = dasherize(modelType);
+        const normalized = this.store.push(this.store.normalize(type, attributes));
+ 
+        return normalized;
+    }
+ 
+    /**
+     * Parses the JSON returned by a network request
+     *
+     * @param  {Object} response A response from a network request
+     * @return {Object}          The parsed JSON, status from the response
+     *
+     * @return {Promise}
+     */
+    async parseJSON(response) {
+        try {
+            const compressedHeader = await response.headers.get('x-compressed-json');
+            let json;
+ 
+            if (compressedHeader === '1') {
+                // Handle compressed json
+                const text = await response.text();
+                json = JSON.parse(text);
+                json = decompressJson(json);
+                json = JSON.parse(json);
+            } else {
+                // Handle regular json
+                json = await response.json();
+            }
+ 
+            return {
+                statusText: response.statusText,
+                status: response.status,
+                ok: response.ok,
+                json,
+            };
+        } catch (error) {
+            throw new Error('Error processing response: ' + error.message);
+        }
+    }
+ 
+    /**
+     * The base request method
+     *
+     * @param {String} path
+     * @param {String} method
+     * @param {Object} data
+     * @param {Object} options
+     *
+     * @return {Promise}
+     */
+    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;
+        const url = options.externalRequest === true ? path : [host, namespace, path].filter(Boolean).join('/');
+ 
+        return new Promise((resolve, reject) => {
+            return fetch(url, {
+                method,
+                mode: options.mode || 'cors',
+                credentials: options.credentials || this.credentials,
+                headers,
+                ...data,
+            })
+                .then(this.parseJSON)
+                .then((response) => {
+                    if (response.ok) {
+                        if (options.normalizeToEmberData) {
+                            const normalized = this.normalizeModel(response.json, options.normalizeModelType);
+ 
+                            if (typeof options.onSuccess === 'function') {
+                                options.onSuccess(normalized);
+                            }
+ 
+                            return resolve(normalized);
+                        }
+ 
+                        if (typeof options.onSuccess === 'function') {
+                            options.onSuccess(response.json);
+                        }
+ 
+                        return resolve(response.json);
+                    }
+ 
+                    if (typeof options.onError === 'function') {
+                        options.onError(response.json);
+                    }
+ 
+                    if (options.rawError) {
+                        return reject(response.json);
+                    }
+ 
+                    if (isArray(response.json.errors)) {
+                        // Decoded JSON is a plain array, so `firstObject` was
+                        // undefined and every such error surfaced as the
+                        // literal string "undefined".
+                        return reject(new Error(response.json.errors[0] ?? response.statusText));
+                    }
+ 
+                    if (response.json.error && typeof response.json.error === 'string') {
+                        return reject(new Error(response.json.error));
+                    }
+ 
+                    // error objects
+                    if (response.json.error && isObject(response.json.error)) {
+                        return reject(response.json.error);
+                    }
+ 
+                    if (response.json.message && typeof response.json.message === 'string') {
+                        return reject(new Error(response.json.message));
+                    }
+ 
+                    return reject(response.json);
+                })
+                .catch(reject);
+        });
+    }
+ 
+    /**
+     * Makes a GET request with fetch
+     *
+     * @param {String} path
+     * @param {Object} query
+     * @param {Object} options
+     *
+     * @return {Promise}
+     */
+    get(path, query = {}, options = {}) {
+        // handle if want to request from cache
+        if (options.fromCache === true) {
+            return this.cachedGet(...arguments);
+        }
+ 
+        const urlParams = !isEmptyObject(query) ? new URLSearchParams(query).toString() : '';
+ 
+        return this.request(`${path}${urlParams ? '?' + urlParams : ''}`, 'GET', {}, options);
+    }
+ 
+    /**
+     * Makes a GET request with fetch, but if the fetch is stored in local cache,
+     * retrieve from storage to prevent unnecessary netwrok request
+     *
+     * @param {String} path
+     * @param {Object} query
+     * @param {Object} options
+     *
+     * @return {Promise}
+     */
+    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) => {
+                // cache the response
+                this.localCache.set(pathKey, response);
+                this.localCache.set(`${pathKey}-version`, pathKeyVersion);
+ 
+                // return response
+                return response;
+            });
+        };
+ 
+        // check to see if in storage already
+        if (this.localCache.get(pathKey)) {
+            return new Promise((resolve) => {
+                // get cached data
+                const data = this.localCache.get(pathKey);
+ 
+                // get the path key version value
+                const version = this.localCache.get(`${pathKey}-version`);
+                const expirationInterval = options.expirationInterval ?? 3;
+                const expirationIntervalUnit = pluralize(options.expirationIntervalUnit ?? 'days');
+ 
+                // Expire once the whole interval has passed since the cache version. (The
+                // interval's unit component alone would read a month-old cache as 0 days old.)
+                const shouldExpire = isAfter(new Date(), add(parseISO(version), { [expirationIntervalUnit]: expirationInterval }));
+ 
+                // if the version is older than 3 days clear it
+                if (!version || shouldExpire || options.clearData === true) {
+                    this.flushRequestCache(path);
+                    return request().then(resolve);
+                }
+ 
+                if (options.normalizeToEmberData) {
+                    return resolve(this.normalizeModel(data, options.normalizeModelType));
+                }
+ 
+                // return cached response
+                return resolve(data);
+            });
+        }
+ 
+        // if no cached data request from server
+        return request();
+    }
+ 
+    /**
+     * 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);
+    }
+ 
+    /**
+     * 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) {
+            this.localCache.clear();
+            this.localCache.set('console-version', config.APP.version);
+        }
+    }
+ 
+    /**
+     * Makes a POST request with fetch
+     *
+     * @param {String} path
+     * @param {Object} data
+     * @param {Object} options
+     *
+     * @return {Promise}
+     */
+    post(path, data = {}, options = {}) {
+        return this.request(path, 'POST', { body: JSON.stringify(data) }, options);
+    }
+ 
+    /**
+     * Makes a PUT request with fetch
+     *
+     * @param {String} path
+     * @param {Object} data
+     * @param {Object} options
+     *
+     * @return {Promise}
+     */
+    put(path, data = {}, options = {}) {
+        return this.request(path, 'PUT', { body: JSON.stringify(data) }, options);
+    }
+ 
+    /**
+     * Makes a DELETE request with fetch
+     *
+     * @param {String} path
+     * @param {Object} data
+     * @param {Object} options
+     *
+     * @return {Promise}
+     */
+    delete(path, data = {}, options = {}) {
+        return this.request(path, 'DELETE', { body: JSON.stringify(data) }, options);
+    }
+ 
+    /**
+     * Makes a PATCH request with fetch
+     * @param {String} path
+     * @param {Object} data
+     * @param {Object} options
+     *
+     * @return {Promise}
+     */
+    patch(path, data = {}, options = {}) {
+        return this.request(path, 'PATCH', { body: JSON.stringify(data) }, options);
+    }
+ 
+    /**
+     * Makes a upload request with fetch
+     *
+     * @param {String} path
+     * @param {Array} files
+     * @param {Object} options
+     *
+     * @return {Promise}
+     */
+    upload(path, files = [], options = {}) {
+        const body = new FormData();
+        files.forEach((file) => {
+            body.append('file', file);
+        });
+        return this.request(path, 'POST', { body }, options);
+    }
+ 
+    /**
+     * Sends request to routing service.
+     *
+     * @param {Array} coordinates
+     * @param {Object} query
+     * @param {String} service
+     * @param {String} profile
+     * @param {String} version
+     */
+    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 params = !isEmptyObject(query) ? new URLSearchParams(query).toString() : '';
+        let path = `${host}/${service}/${version}/${profile}/${route}`;
+        let url = `${path}${params ? '?' + params : ''}`;
+ 
+        return new Promise((resolve, reject) => {
+            corslite(url, (container, xhr) => {
+                if (!xhr || !xhr.response) {
+                    reject(new Error('Request failed.'));
+                    return;
+                }
+ 
+                let response = xhr.response;
+                let isJson = typeof response === 'string' && response.startsWith('{');
+ 
+                resolve(isJson ? JSON.parse(response) : response);
+            });
+        });
+    }
+ 
+    /**
+     * Concurrency task to handle a file upload
+     *
+     * @void
+     */
+    @task({ enqueue: true, maxConcurrency: 3 }) *uploadFile(file, params = {}, callback, errorCallback) {
+        const { queue } = file;
+        const headers = this.getHeaders();
+ 
+        // make sure this task runs once for this file in correct state
+        // this can occur when the task is called twice when upload button exists inside upload dropzone
+        if (['queued', 'failed', 'timed_out', 'aborted'].indexOf(file.state) === -1) return;
+ 
+        // remove Content-Type header
+        delete headers['Content-Type'];
+ 
+        try {
+            const upload = yield file
+                .upload(`${get(config, 'API.host')}/${get(config, 'API.namespace')}/files/upload`, {
+                    data: {
+                        ...params,
+                        file_size: file.size,
+                    },
+                    withCredentials: true,
+                    headers,
+                })
+                .then((response) => response.json())
+                .catch((error) => {
+                    this.notifications.serverError(error, 'File upload failed.');
+ 
+                    if (typeof errorCallback === 'function') {
+                        errorCallback(error);
+                    }
+                });
+ 
+            if (upload) {
+                const model = this.store.push(this.store.normalize('file', upload.file));
+                set(file, 'model', model);
+ 
+                if (typeof callback === 'function') {
+                    callback(model);
+                }
+ 
+                return model;
+            }
+ 
+            return null;
+        } catch (error) {
+            queue.remove(file);
+            this.notifications.serverError(error, 'File upload failed.');
+ 
+            if (typeof errorCallback === 'function') {
+                errorCallback(error);
+            }
+        }
+    }
+ 
+    /**
+     * Downloads blob of the request path to user
+     *
+     * @param {String} path
+     * @param {Object} query
+     * @param {Object} options
+     *
+     * @return {Promise}
+     */
+    download(path, query = {}, options = {}) {
+        const headers = Object.assign(this.getHeaders(), options.headers ?? {});
+        const method = options.method ?? 'GET';
+        const credentials = options.credentials ?? this.credentials;
+        const baseUrl = options.externalRequest === true ? '' : `${options.host ?? this.host}/${options.namespace || this.namespace}`;
+        const isReadOnlyRequest = ['GET', 'HEAD'].includes(method.toUpperCase());
+        const params = isReadOnlyRequest && !isEmptyObject(query) ? `?${new URLSearchParams(query).toString()}` : '';
+        const body = !isReadOnlyRequest ? JSON.stringify(query) : {};
+        const fetchOptions = {
+            method,
+            credentials,
+            headers,
+        };
+ 
+        // Only supply body to fetch if not GET or HEAD request
+        if (!isReadOnlyRequest) {
+            fetchOptions.body = body;
+        }
+ 
+        return new Promise((resolve, reject) => {
+            return fetch(`${baseUrl ? baseUrl + '/' : ''}${path}${params}`, fetchOptions)
+                .then((response) => {
+                    options.fileName = this.getFilenameFromResponse(response, options.fileName);
+                    options.mimeType = this.getMimeTypeFromResponse(response, options.mimeType);
+ 
+                    if (!options.mimeType) {
+                        options.mimeType = getMimeType(options.fileName);
+                    }
+ 
+                    return response;
+                })
+                .then((response) => response.blob())
+                .then((blob) => resolve(download(blob, options.fileName, options.mimeType)))
+                .catch((error) => {
+                    reject(error);
+                });
+        });
+    }
+ 
+    getFilenameFromResponse(response, defaultFilename = null) {
+        const contentDisposition = response.headers.get('content-disposition');
+        let fileName = defaultFilename;
+ 
+        // A name the caller asked for wins; the header only fills in when there is none.
+        if (contentDisposition && !defaultFilename) {
+            const results = /filename=(.*)/.exec(contentDisposition);
+ 
+            if (isArray(results) && results.length > 1) {
+                fileName = results[1];
+ 
+                // clean fileName
+                fileName = fileName.replaceAll('"', '');
+            }
+        }
+ 
+        return fileName;
+    }
+ 
+    getMimeTypeFromResponse(response, defaultMimeType = null) {
+        const contentType = response.headers.get('content-type');
+        let mimeType = defaultMimeType;
+ 
+        if (contentType) {
+            // The media type is everything before the first parameter, if there are any:
+            // `text/csv` and `text/csv; charset=utf-8` both mean `text/csv`.
+            mimeType = contentType.split(';')[0].trim();
+        }
+ 
+        return mimeType;
+    }
+ 
+    fetchOrderConfigurations(params = {}) {
+        return new Promise((resolve, reject) => {
+            this.request('fleet-ops/order-configs/get-installed', params)
+                .then((configs) => {
+                    const serialized = [];
+ 
+                    for (let i = 0; i < configs.length; i++) {
+                        // `configs` is decoded JSON and `serialized` is a plain
+                        // array literal; neither has the Ember array methods
+                        // once prototype extensions are off.
+                        const config = configs[i];
+                        const normalizedConfig = this.store.normalize('order-config', config);
+                        const serializedConfig = this.store.push(normalizedConfig);
+ 
+                        serialized.push(serializedConfig);
+                    }
+ 
+                    resolve(serialized);
+                })
+                .catch((error) => {
+                    reject(error);
+                });
+        });
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/index.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/index.html new file mode 100644 index 0000000..904317d --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/index.html @@ -0,0 +1,206 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services + + + + + + + + + +
+
+

All files ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services

+
+ +
+ 0.14% + Statements + 1/709 +
+ + +
+ 0.21% + Branches + 1/467 +
+ + +
+ 0% + Functions + 0/196 +
+ + +
+ 0.14% + Lines + 1/692 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
current-user.js +
+
0%0/1500%0/660%0/290%0/148
events.js +
+
0%0/750%0/570%0/170%0/75
fetch.js +
+
0.47%1/2120.53%1/1870%0/460.48%1/206
notifications.js +
+
0%0/130%0/120%0/20%0/13
session.js +
+
0%0/680%0/240%0/190%0/68
universe.js +
+
0%0/1390%0/1120%0/630%0/130
url-search-params.js +
+
0%0/520%0/90%0/200%0/52
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/notifications.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/notifications.js.html new file mode 100644 index 0000000..1624d27 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/notifications.js.html @@ -0,0 +1,184 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/notifications.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services notifications.js

+
+ +
+ 0% + Statements + 0/13 +
+ + +
+ 0% + Branches + 0/12 +
+ + +
+ 0% + Functions + 0/2 +
+ + +
+ 0% + Lines + 0/13 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import EmberNotificationsService from 'ember-cli-notifications/services/notifications';
+import { isArray } from '@ember/array';
+import getWithDefault from '../utils/get-with-default';
+ 
+export default class NotificationsService extends EmberNotificationsService {
+    serverError(error, fallbackMessage = 'Oops! Something went wrong with your request.', options = {}) {
+        if (error && isArray(error.errors)) {
+            const errors = getWithDefault(error, 'errors');
+            const errorMessage = getWithDefault(errors, '0', fallbackMessage);
+ 
+            return this.error(errorMessage, options);
+        }
+ 
+        if (error instanceof Error) {
+            const errorMessage = getWithDefault(error, 'message', fallbackMessage);
+            return this.error(errorMessage, options);
+        }
+ 
+        if (typeof error === 'string') {
+            return this.error(error, options);
+        }
+ 
+        return this.error(fallbackMessage, options);
+    }
+ 
+    invoke(type, message, ...params) {
+        if (typeof message === 'function') {
+            this[type](message(...params));
+        } else {
+            this[type](message);
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/session.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/session.js.html new file mode 100644 index 0000000..0f60dfd --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/session.js.html @@ -0,0 +1,1093 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/session.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services session.js

+
+ +
+ 0% + Statements + 0/68 +
+ + +
+ 0% + Branches + 0/24 +
+ + +
+ 0% + Functions + 0/19 +
+ + +
+ 0% + Lines + 0/68 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
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';
+ 
+/**
+ * SessionService
+ *
+ * Extends ember-simple-auth's session service to add:
+ *
+ *   - Proper lifecycle event firing through the EventsService (which
+ *     re-broadcasts on both the events bus and the universe bus).
+ *   - Session start timestamp tracking for duration calculation.
+ *
+ * Session lifecycle events fired (via EventsService → universe bus):
+ *
+ *   session.authenticated   — fired after a successful login/session restore.
+ *                             Payload: (properties)
+ *
+ *   session.invalidated     — fired after the session is destroyed (logout).
+ *                             Payload: (duration_seconds, properties)
+ *
+ *   user.deauthenticated    — alias for session.invalidated, provided as a
+ *                             semantic convenience for integrations that want
+ *                             to react to the user identity being cleared
+ *                             (e.g. Intercom shutdown, PostHog reset).
+ *                             Payload: (duration_seconds, properties)
+ *
+ * All events are fired on both the EventsService Evented bus and the universe
+ * service, so listeners can use either:
+ *
+ *   this.events.on('session.authenticated', handler)
+ *   this.universe.on('session.authenticated', handler)   ← recommended for engines
+ */
+export default class SessionService extends SimpleAuthSessionService {
+    @service router;
+    @service currentUser;
+    @service fetch;
+    @service notifications;
+    @service events;
+    @service universe;
+ 
+    /**
+     * Set where to transition to
+     *
+     * @var {String}
+     */
+    @tracked redirectTo = 'console';
+ 
+    /**
+     * If session is onboarding a user.
+     *
+     * @var {String}
+     */
+    @tracked _isOnboarding = false;
+ 
+    /**
+     * Timestamp (ms) when the session was authenticated.
+     * Used to calculate session duration on invalidation.
+     *
+     * @var {Number|null}
+     */
+    @tracked _sessionStartedAt = null;
+ 
+    /**
+     * Set this as onboarding.
+     *
+     * @return {SessionService}
+     */
+    isOnboarding() {
+        this._isOnboarding = true;
+ 
+        return this;
+    }
+ 
+    /**
+     * Manually authenticate user
+     */
+    manuallyAuthenticate(authToken) {
+        return this.session._setup('authenticator:fleetbase', { token: authToken }, true);
+    }
+ 
+    /**
+     * Overwrite the handle authentication method.
+     *
+     * Fires `session.authenticated` through the events service so that
+     * integrations (Intercom, PostHog, etc.) can react to a successful login.
+     *
+     * @void
+     */
+    async handleAuthentication() {
+        // Record session start time for duration tracking on logout
+        this._sessionStartedAt = Date.now();
+ 
+        // Fire session.authenticated event
+        this._fireSessionEvent('session.authenticated');
+ 
+        if (this._isOnboarding) {
+            return;
+        }
+ 
+        const loaderNode = this.showLoader('Starting session...');
+        const removeLoaderNode = () => {
+            later(
+                this,
+                () => {
+                    // remove node from body
+                    document.body.removeChild(loaderNode);
+                    this.isLoaderNodeOpen = false;
+                },
+                600 * 3
+            );
+        };
+        this.isLoaderNodeOpen = true;
+ 
+        try {
+            await this.router.transitionTo(this.redirectTo);
+        } catch (error) {
+            debug(`Session's handleAuthentication() failed to transition: ${error.message}`);
+        }
+ 
+        removeLoaderNode();
+    }
+ 
+    /**
+     * Extends the parent handleInvalidation method.
+     *
+     * IMPORTANT: super.handleInvalidation(routeAfterInvalidation) is called
+     * first to preserve the ember-simple-auth behaviour of redirecting the
+     * user to the login page (via handleSessionInvalidated). Our event
+     * firing happens after so it cannot interfere with the redirect.
+     *
+     * @param {String} routeAfterInvalidation - Passed through from ember-simple-auth
+     * @void
+     */
+    handleInvalidation(routeAfterInvalidation) {
+        // 1. Always call super first — this performs the actual post-logout
+        //    redirect/reload that ember-simple-auth is responsible for.
+        super.handleInvalidation(routeAfterInvalidation);
+ 
+        const durationSeconds = this._sessionStartedAt ? Math.round((Date.now() - this._sessionStartedAt) / 1000) : null;
+ 
+        // 2. Fire session lifecycle events for integrations (Intercom, PostHog, etc.)
+        if (this.events) {
+            this.events.trackSessionTerminated(durationSeconds);
+        }
+ 
+        // 3. Fire user.deauthenticated directly on universe for framework-level
+        //    uniformity — engines can listen without needing the events service.
+        this._fireSessionEvent('user.deauthenticated', { session_duration: durationSeconds });
+ 
+        // Reset session start time
+        this._sessionStartedAt = null;
+    }
+ 
+    /**
+     * Loads the current authenticated user
+     *
+     * @void
+     */
+    async loadCurrentUser() {
+        try {
+            const user = await this.currentUser.load();
+ 
+            if (!user) {
+                return this.invalidateWithLoader('Session authentication failed...');
+            }
+ 
+            return user;
+        } catch (error) {
+            await this.invalidateWithLoader(getWithDefault(error, 'message', 'Session authentication failed...'));
+        }
+    }
+ 
+    /**
+     * Loads the current authenticated user
+     *
+     * @param {Transition} transition
+     * @void
+     */
+    async promiseCurrentUser(transition = null) {
+        const invalidateWithLoader = this.invalidateWithLoader.bind(this);
+ 
+        // Abort and invalidate once, whichever way authentication failed.
+        const fail = async (error) => {
+            if (transition) {
+                transition.abort();
+            }
+ 
+            await invalidateWithLoader(error.message ?? 'Session authentication failed...');
+            throw error;
+        };
+ 
+        let user;
+        try {
+            user = await this.currentUser.promiseUser();
+        } catch (error) {
+            return fail(error);
+        }
+ 
+        if (!user) {
+            return fail(new Error('Session authentication failed...'));
+        }
+ 
+        return user;
+    }
+ 
+    /**
+     * Creates an HTML element node for a loading overlay with a message.
+     *
+     * @param {String} loadingMessage
+     * @return {HTMLElement} loader
+     */
+    showLoader(loadingMessage) {
+        const loader = document.createElement('div');
+        loader.classList.add('overloader');
+        loader.innerHTML = `<div class="flex items-center justify-center">
+            <div>
+                <svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" role="img" focusable="false" aria-hidden="true" data-icon="spinner-third" data-prefix="fad" id="ember240" class="svg-inline--fa fa-spinner-third fa-w-16 fa-spin ember-view text-sky-500 fa-spin-800ms mr-3"><g class="fa-group"><path class="fa-secondary" fill="currentColor" d="M478.71 364.58zm-22 6.11l-27.83-15.9a15.92 15.92 0 0 1-6.94-19.2A184 184 0 1 1 256 72c5.89 0 11.71.29 17.46.83-.74-.07-1.48-.15-2.23-.21-8.49-.69-15.23-7.31-15.23-15.83v-32a16 16 0 0 1 15.34-16C266.24 8.46 261.18 8 256 8 119 8 8 119 8 256s111 248 248 248c98 0 182.42-56.95 222.71-139.42-4.13 7.86-14.23 10.55-22 6.11z"></path><path class="fa-primary" fill="currentColor" d="M271.23 72.62c-8.49-.69-15.23-7.31-15.23-15.83V24.73c0-9.11 7.67-16.78 16.77-16.17C401.92 17.18 504 124.67 504 256a246 246 0 0 1-25 108.24c-4 8.17-14.37 11-22.26 6.45l-27.84-15.9c-7.41-4.23-9.83-13.35-6.2-21.07A182.53 182.53 0 0 0 440 256c0-96.49-74.27-175.63-168.77-183.38z"></path></g>
+                </svg>
+            </div>
+ 
+            <span class="font-semibold text-gray-700 dark:text-gray-100 test-sm">${loadingMessage}</span>
+        </div>`;
+        document.body.appendChild(loader);
+ 
+        return loader;
+    }
+ 
+    /**
+     * Invalidates the current session while displaying a loading message on the page.
+     *
+     * @param {String} loadingMessage
+     * @return {Promise}
+     */
+    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 = true;
+ 
+        return this.session.invalidate().then(() => {
+            later(
+                this,
+                () => {
+                    document.body.removeChild(loaderNode);
+                    this.isLoaderNodeOpen = false;
+                },
+                600
+            );
+        });
+    }
+ 
+    /**
+     * Set the redirect route after authentication
+     *
+     * @void
+     */
+    setRedirect(whereTo = 'console') {
+        this.redirectTo = whereTo;
+    }
+ 
+    /**
+     * Get session time expiry date in moment
+     *
+     * @return {Date}
+     */
+    getExpiresAtDate() {
+        return new Date(this.data.authenticated.expires_at);
+    }
+ 
+    /**
+     * Get session time expiry in seconds
+     *
+     * @return {Integer}
+     */
+    getSessionSecondsRemaining() {
+        const date = this.getExpiresAtDate();
+        const now = new Date();
+ 
+        return Math.round((date - now) / 1000);
+    }
+ 
+    /**
+     * Checks for the presence of two-factor authentication for a given user identity.
+     *
+     * @param {String} identity
+     * @return {Promise}
+     * @throws {Error}
+     */
+    checkForTwoFactor(identity) {
+        return this.fetch.get('two-fa/check', { identity }).catch((error) => {
+            throw new Error(error.message);
+        });
+    }
+ 
+    // =========================================================================
+    // Private helpers
+    // =========================================================================
+ 
+    /**
+     * Fires a named session lifecycle event on both the events service and
+     * directly on the universe service.
+     *
+     * Firing on universe directly (in addition to via events service) ensures
+     * that all engines and extensions receive the event on the shared framework-
+     * level bus regardless of whether the events service has fully initialised.
+     *
+     * Listeners can subscribe via:
+     *   this.universe.on('session.authenticated', handler)
+     *   this.universe.on('user.deauthenticated', handler)
+     *   this.events.on('session.authenticated', handler)
+     *
+     * @private
+     * @param {String} eventName
+     * @param {Object} [extraProps={}]
+     */
+    _fireSessionEvent(eventName, extraProps = {}) {
+        // 1. Fire through the events service (dual-broadcasts on events + universe bus)
+        if (this.events) {
+            this.events.trackEvent(eventName, extraProps);
+        }
+ 
+        // 2. Also trigger directly on universe for framework-level uniformity —
+        //    ensures the event reaches all engines even if events service is
+        //    not yet available or not injected in a given engine context.
+        if (this.universe) {
+            this.universe.trigger(eventName, extraProps);
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe.js.html new file mode 100644 index 0000000..40cf883 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe.js.html @@ -0,0 +1,2848 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services universe.js

+
+ +
+ 0% + Statements + 0/139 +
+ + +
+ 0% + Branches + 0/112 +
+ + +
+ 0% + Functions + 0/63 +
+ + +
+ 0% + Lines + 0/130 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import Service from '@ember/service';
+import Evented from '@ember/object/evented';
+import { inject as service } from '@ember/service';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import { getOwner } from '@ember/application';
+import { A } from '@ember/array';
+import MenuItem from '../contracts/menu-item';
+ 
+/**
+ * UniverseService (Refactored)
+ *
+ * This is the new UniverseService that acts as a facade to the specialized sub-services.
+ * It maintains backward compatibility with the old API while delegating to the new architecture.
+ *
+ * The service decomposition provides:
+ * - ExtensionManager: Handles lazy loading of engines
+ * - RegistryService: Manages all registries using Ember's container
+ * - MenuService: Manages menu items and panels
+ * - WidgetService: Manages dashboard widgets
+ * - HookService: Manages application hooks
+ *
+ * @class UniverseService
+ * @extends Service
+ */
+export default class UniverseService extends Service.extend(Evented) {
+    // Inject specialized services
+    @service('universe/extension-manager') extensionManager;
+    @service('universe/registry-service') registryService;
+    @service('universe/menu-service') menuService;
+    @service('universe/widget-service') widgetService;
+    @service('universe/hook-service') hookService;
+    @service('universe/resource-view-service') resourceViewService;
+    @service router;
+    @service intl;
+    @service urlSearchParams;
+    @tracked applicationInstance;
+    @tracked initialLocation = { ...window.location };
+    @tracked bootCallbacks = A([]);
+ 
+    /**
+     * Set the application instance on this service and cascade to RegistryService
+     * Called by the instance initializer to ensure both services have access
+     * to the root application container
+     *
+     * @method setApplicationInstance
+     * @param {Application} application The root application instance
+     */
+    setApplicationInstance(application) {
+        this.applicationInstance = application;
+ 
+        // Cascade to all child services
+        if (this.registryService) {
+            this.registryService.setApplicationInstance(application);
+        }
+        if (this.extensionManager) {
+            this.extensionManager.setApplicationInstance(application);
+        }
+        if (this.menuService) {
+            this.menuService.setApplicationInstance(application);
+        }
+        if (this.widgetService) {
+            this.widgetService.setApplicationInstance(application);
+        }
+        if (this.hookService) {
+            this.hookService.setApplicationInstance(application);
+        }
+    }
+ 
+    /**
+     * Get a service by name
+     * Convenience method for extensions to access specialized services
+     *
+     * Supports multiple naming patterns:
+     * - "universe/menu-service" -> universe/menu-service
+     * - "menu-service" -> universe/menu-service
+     * - "menuService" -> universe/menu-service
+     * - "menu" -> universe/menu-service
+     * - "hooks" or "hook" -> universe/hook-service
+     * - "widgets" or "widget" -> universe/widget-service
+     * - "registry" -> universe/registry-service
+     * - "resource-view" or "resourceView" -> universe/resource-view-service
+     *
+     * @method getService
+     * @param {String} serviceName Service name in various formats
+     * @returns {Service} The service instance
+     */
+    getService(serviceName) {
+        const owner = getOwner(this);
+        let resolvedName = serviceName;
+ 
+        // Normalize the service name
+        if (!/\//.test(serviceName)) {
+            // No slash, might be camelCase or short name
+            const kebabCase = serviceName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
+ 
+            // Map short names and variations to full service names
+            const nameMapping = {
+                hook: 'hook-service',
+                hooks: 'hook-service',
+                'hook-service': 'hook-service',
+                menu: 'menu-service',
+                'menu-service': 'menu-service',
+                widget: 'widget-service',
+                widgets: 'widget-service',
+                'widget-service': 'widget-service',
+                registry: 'registry-service',
+                'registry-service': 'registry-service',
+                'resource-view': 'resource-view-service',
+                'resource-views': 'resource-view-service',
+                'resource-view-service': 'resource-view-service',
+            };
+ 
+            const mappedName = nameMapping[kebabCase] || kebabCase;
+            resolvedName = `universe/${mappedName}`;
+        } else if (serviceName.startsWith('universe/')) {
+            // Already has universe/ prefix, ensure it's using -service naming
+            resolvedName = serviceName;
+        }
+ 
+        return owner.lookup(`service:${resolvedName}`);
+    }
+ 
+    // ============================================================================
+    // Extension Management (delegates to ExtensionManager)
+    // ============================================================================
+ 
+    /**
+     * Ensure an engine is loaded
+     *
+     * @method ensureEngineLoaded
+     * @param {String} engineName Engine name
+     * @returns {Promise<EngineInstance>} Engine instance
+     */
+    async ensureEngineLoaded(engineName) {
+        return this.extensionManager.ensureEngineLoaded(engineName);
+    }
+ 
+    /**
+     * Get an engine instance
+     *
+     * @method getEngineInstance
+     * @param {String} engineName Engine name
+     * @returns {EngineInstance|null} Engine instance or null
+     */
+    getEngineInstance(engineName) {
+        return this.extensionManager.getEngineInstance(engineName);
+    }
+ 
+    /**
+     * Register an extension
+     *
+     * @method registerExtension
+     * @param {String} name Extension name
+     * @param {Object} metadata Extension metadata
+     */
+    registerExtension(name, metadata = {}) {
+        this.extensionManager.registerExtension(name, metadata);
+    }
+ 
+    /**
+     * Listen for a specific engine to be loaded
+     * Note: This uses event listeners and will NOT run if the engine is already loaded.
+     * Use whenEngineLoaded() if you want to handle both cases.
+     *
+     * @method onEngineLoaded
+     * @param {String} engineName The engine name to listen for
+     * @param {Function} callback Function to call when the engine loads, receives engineInstance as parameter
+     * @example
+     * universe.onEngineLoaded('@fleetbase/fleetops-engine', (engineInstance) => {
+     *     console.log('FleetOps engine loaded!', engineInstance);
+     * });
+     */
+    onEngineLoaded(engineName, callback) {
+        this.extensionManager.on('engine.loaded', (name, instance) => {
+            if (name === engineName) {
+                callback(instance);
+            }
+        });
+    }
+ 
+    /**
+     * Execute a callback when an engine is loaded
+     * If the engine is already loaded, the callback runs immediately
+     * Otherwise, it's stored and runs when the engine loads
+     *
+     * This is the recommended way to handle engine-dependent setup.
+     *
+     * @method whenEngineLoaded
+     * @param {String} engineName The engine name
+     * @param {Function} callback Function to call, receives (engineInstance, universe, appInstance)
+     * @example
+     * // Replaces this pattern:
+     * if (universe.extensionManager.isEngineLoaded('@fleetbase/fleetops-engine')) {
+     *     const engine = universe.extensionManager.getEngineInstance('@fleetbase/fleetops-engine');
+     *     doSomething(engine);
+     * } else {
+     *     universe.onEngineLoaded('@fleetbase/fleetops-engine', (engine) => {
+     *         doSomething(engine);
+     *     });
+     * }
+     *
+     * // With this simpler pattern:
+     * universe.whenEngineLoaded('@fleetbase/fleetops-engine', (engine) => {
+     *     doSomething(engine);
+     * });
+     */
+    whenEngineLoaded(engineName, callback) {
+        return this.extensionManager.whenEngineLoaded(engineName, callback);
+    }
+ 
+    /**
+     * Get the application instance
+     *
+     * @method getApplicationInstance
+     * @returns {ApplicationInstance} The application instance
+     */
+    getApplicationInstance() {
+        return this.applicationInstance;
+    }
+ 
+    /**
+     * Get a service from a specific engine
+     *
+     * @method getServiceFromEngine
+     * @param {String} engineName The engine name
+     * @param {String} serviceName The service name
+     * @param {Object} options Optional options
+     * @param {Object} options.inject Properties to inject into the service
+     * @returns {Service|null} The service instance or null
+     * @example
+     * const userService = universe.getServiceFromEngine('user-engine', 'user');
+     * if (userService) {
+     *     userService.doSomething();
+     * }
+     */
+    getServiceFromEngine(engineName, serviceName, options = {}) {
+        const engineInstance = this.getEngineInstance(engineName);
+ 
+        if (engineInstance && typeof serviceName === 'string') {
+            const serviceInstance = engineInstance.lookup(`service:${serviceName}`);
+            if (options && options.inject) {
+                for (let injectionName in options.inject) {
+                    serviceInstance[injectionName] = options.inject[injectionName];
+                }
+            }
+            return serviceInstance;
+        }
+ 
+        return null;
+    }
+ 
+    // ============================================================================
+    // Registry Management (delegates to RegistryService)
+    // ============================================================================
+ 
+    /**
+     * Create a new registry
+     *
+     * @method createRegistry
+     * @param {String} name Registry name
+     * @returns {Array} The created registry
+     */
+    createRegistry(name) {
+        return this.registryService.createRegistry(name);
+    }
+ 
+    /**
+     * Create multiple registries
+     *
+     * @method createRegistries
+     * @param {Array} names Array of registry names
+     */
+    createRegistries(names) {
+        this.registryService.createRegistries(names);
+    }
+ 
+    /**
+     * Get a registry
+     *
+     * @method getRegistry
+     * @param {String} name Registry name
+     * @param {String} listName The list within the registry (default 'menu-item', as createRegistry)
+     * @returns {Array} Registry items
+     */
+    getRegistry(name, listName = 'menu-item') {
+        return this.registryService.getRegistry(name, listName);
+    }
+ 
+    /**
+     * Register an item to a registry
+     *
+     * @method registerInRegistry
+     * @param {String} registryName Registry name
+     * @param {String} key Item key
+     * @param {*} value Item value
+     * @param {String} listName The list within the registry (default 'menu-item', as createRegistry)
+     */
+    registerInRegistry(registryName, key, value, listName = 'menu-item') {
+        this.registryService.register(registryName, listName, key, value);
+    }
+ 
+    /**
+     * Lookup an item from a registry
+     *
+     * @method lookupFromRegistry
+     * @param {String} registryName Registry name
+     * @param {String} key Item key
+     * @param {String} listName The list within the registry (default 'menu-item', as createRegistry)
+     * @returns {*} The registered item
+     */
+    lookupFromRegistry(registryName, key, listName = 'menu-item') {
+        return this.registryService.lookup(registryName, listName, key);
+    }
+ 
+    // ============================================================================
+    // Resource View Registries (delegates to ResourceViewService)
+    // ============================================================================
+ 
+    /**
+     * Register columns, actions or buttons into a table or details view slot.
+     *
+     * @method registerInResourceView
+     * @param {String} registryName e.g. 'fleet-ops:driver:table:columns'
+     * @param {Object|Array} items TableColumn, ResourceAction or ActionButton contracts
+     * @returns {Boolean}
+     *
+     * @example
+     * universe.registerInResourceView('ledger:invoice:details:menu', new ResourceAction({ id: 'acme-print', label: 'Print', fn: (invoice) => … }));
+     */
+    registerInResourceView(registryName, items) {
+        return this.resourceViewService.register(registryName, items);
+    }
+ 
+    registerTableColumn(extension, resource, column) {
+        return this.resourceViewService.registerTableColumn(extension, resource, column);
+    }
+ 
+    registerRowAction(extension, resource, action) {
+        return this.resourceViewService.registerRowAction(extension, resource, action);
+    }
+ 
+    registerBulkAction(extension, resource, action) {
+        return this.resourceViewService.registerBulkAction(extension, resource, action);
+    }
+ 
+    registerTableAction(extension, resource, button) {
+        return this.resourceViewService.registerTableAction(extension, resource, button);
+    }
+ 
+    registerDetailsAction(extension, resource, button) {
+        return this.resourceViewService.registerDetailsAction(extension, resource, button);
+    }
+ 
+    registerDetailsMenuItem(extension, resource, action) {
+        return this.resourceViewService.registerDetailsMenuItem(extension, resource, action);
+    }
+ 
+    // ============================================================================
+    // Application Container Registration (delegates to RegistryService)
+    // ============================================================================
+ 
+    /**
+     * Registers a component to the root application container.
+     * This ensures the component is available to all engines and the host app.
+     * @method registerComponent
+     * @param {String} name The component name (e.g., 'my-component')
+     * @param {Class} componentClass The component class
+     * @param {Object} options Registration options
+     */
+    registerComponent(name, componentClass, options = {}) {
+        this.registryService.registerComponent(name, componentClass, options);
+    }
+ 
+    /**
+     * Registers a service to the root application container.
+     * This ensures the service is available to all engines and the host app.
+     * @method registerService
+     * @param {String} name The service name (e.g., 'my-service')
+     * @param {Class} serviceClass The service class
+     * @param {Object} options Registration options
+     */
+    registerService(name, serviceClass, options = {}) {
+        this.registryService.registerService(name, serviceClass, options);
+    }
+ 
+    // ============================================================================
+    // Menu Management (delegates to MenuService)
+    // ============================================================================
+ 
+    /**
+     * Register a header menu item
+     *
+     * @method registerHeaderMenuItem
+     * @param {MenuItem|String} menuItemOrTitle MenuItem instance or title
+     * @param {String} route Optional route
+     * @param {Object} options Optional options
+     */
+    registerHeaderMenuItem(menuItemOrTitle, route = null, options = {}) {
+        this.menuService.registerHeaderMenuItem(menuItemOrTitle, route, options);
+    }
+ 
+    /**
+     * Register an organization menu item
+     *
+     * @method registerOrganizationMenuItem
+     * @param {MenuItem|String} menuItemOrTitle MenuItem instance or title
+     * @param {Object} options Optional options
+     */
+    registerOrganizationMenuItem(menuItemOrTitle, options = {}) {
+        this.menuService.registerOrganizationMenuItem(menuItemOrTitle, options);
+    }
+ 
+    /**
+     * Register a user menu item
+     *
+     * @method registerUserMenuItem
+     * @param {MenuItem|String} menuItemOrTitle MenuItem instance or title
+     * @param {Object} options Optional options
+     */
+    registerUserMenuItem(menuItemOrTitle, options = {}) {
+        this.menuService.registerUserMenuItem(menuItemOrTitle, options);
+    }
+ 
+    /**
+     * Register an admin menu panel
+     *
+     * @method registerAdminMenuPanel
+     * @param {MenuPanel|String} panelOrTitle MenuPanel instance or title
+     * @param {Array} items Optional items
+     * @param {Object} options Optional options
+     */
+    registerAdminMenuPanel(panelOrTitle, items = [], options = {}) {
+        this.menuService.registerAdminMenuPanel(panelOrTitle, items, options);
+    }
+ 
+    /**
+     * Register a settings menu item
+     *
+     * @method registerSettingsMenuItem
+     * @param {MenuItem|String} menuItemOrTitle MenuItem instance or title
+     * @param {Object} options Optional options
+     */
+    registerSettingsMenuItem(menuItemOrTitle, options = {}) {
+        this.menuService.registerSettingsMenuItem(menuItemOrTitle, options);
+    }
+ 
+    /**
+     * Register a menu item to a custom registry
+     *
+     * @method registerMenuItem
+     * @param {String} registryName Registry name
+     * @param {MenuItem|String} menuItemOrTitle MenuItem instance or title
+     * @param {String|Object} routeOrOptions Route or options
+     * @param {Object} options Optional options
+     */
+    registerMenuItem(registryName, menuItemOrTitle, routeOrOptions = {}, options = {}) {
+        this.menuService.registerMenuItem(registryName, menuItemOrTitle, routeOrOptions, options);
+    }
+ 
+    /**
+     * Get header menu items
+     *
+     * @computed headerMenuItems
+     * @returns {Array} Header menu items
+     */
+    get headerMenuItems() {
+        return this.menuService.getHeaderMenuItems();
+    }
+ 
+    /**
+     * Get organization menu items
+     *
+     * @computed organizationMenuItems
+     * @returns {Array} Organization menu items
+     */
+    get organizationMenuItems() {
+        return this.menuService.getOrganizationMenuItems();
+    }
+ 
+    /**
+     * Get user menu items
+     *
+     * @computed userMenuItems
+     * @returns {Array} User menu items
+     */
+    get userMenuItems() {
+        return this.menuService.getUserMenuItems();
+    }
+ 
+    /**
+     * Get admin menu items
+     *
+     * @computed adminMenuItems
+     * @returns {Array} Admin menu items
+     */
+    get adminMenuItems() {
+        return this.menuService.getAdminMenuItems();
+    }
+ 
+    /**
+     * Get admin menu panels
+     *
+     * @computed adminMenuPanels
+     * @returns {Array} Admin menu panels
+     */
+    get adminMenuPanels() {
+        return this.menuService.getAdminMenuPanels();
+    }
+ 
+    // ============================================================================
+    // Widget Management (delegates to WidgetService)
+    // ============================================================================
+ 
+    /**
+     * Register default dashboard widgets
+     *
+     * @method registerDefaultDashboardWidgets
+     * @param {Array<Widget>} widgets Array of widgets
+     */
+    registerDefaultDashboardWidgets(widgets) {
+        this.widgetService.registerDefaultDashboardWidgets(widgets);
+    }
+ 
+    /**
+     * Register dashboard widgets
+     *
+     * @method registerDashboardWidgets
+     * @param {Array<Widget>} widgets Array of widgets
+     */
+    registerDashboardWidgets(widgets) {
+        this.widgetService.registerDashboardWidgets(widgets);
+    }
+ 
+    /**
+     * Register a dashboard
+     *
+     * @method registerDashboard
+     * @param {String} name Dashboard name
+     * @param {Object} options Dashboard options
+     */
+    registerDashboard(name, options = {}) {
+        this.widgetService.registerDashboard(name, options);
+    }
+ 
+    /**
+     * Register a dashboard render slot.
+     *
+     * @method registerDashboardSlot
+     * @param {String} slotId Dashboard slot identifier
+     * @param {Object} options Slot options
+     */
+    registerDashboardSlot(slotId, options = {}) {
+        this.widgetService.registerDashboardSlot(slotId, options);
+    }
+ 
+    /**
+     * Register a system dashboard for a render slot.
+     *
+     * @method registerDashboardForSlot
+     * @param {String} slotId Dashboard slot identifier
+     * @param {String} dashboardName Dashboard widget namespace
+     * @param {Object} options Dashboard slot options
+     */
+    registerDashboardForSlot(slotId, dashboardName, options = {}) {
+        this.widgetService.registerDashboardForSlot(slotId, dashboardName, options);
+    }
+ 
+    /**
+     * Set the first system dashboard for a render slot.
+     *
+     * @method setDefaultDashboardForSlot
+     * @param {String} slotId Dashboard slot identifier
+     * @param {String} dashboardName Dashboard widget namespace
+     */
+    setDefaultDashboardForSlot(slotId, dashboardName) {
+        this.widgetService.setDefaultDashboardForSlot(slotId, dashboardName);
+    }
+ 
+    /**
+     * Set the dashboard that should load first on the console home.
+     *
+     * @method setConsoleDashboard
+     * @param {String} dashboardName Dashboard widget namespace
+     */
+    setConsoleDashboard(dashboardName) {
+        this.widgetService.setConsoleDashboard(dashboardName);
+    }
+ 
+    /**
+     * Get dashboard widgets
+     *
+     * @computed dashboardWidgets
+     * @returns {Object} Dashboard widgets object
+     */
+    get dashboardWidgets() {
+        return {
+            // The dashboard the deprecated registerDashboardWidgets() and
+            // registerDefaultDashboardWidgets() register into.
+            defaultWidgets: this.widgetService.getDefaultWidgets('dashboard'),
+            widgets: this.widgetService.getWidgets('dashboard'),
+        };
+    }
+ 
+    // ============================================================================
+    // Hook Management (delegates to HookService)
+    // ============================================================================
+ 
+    /**
+     * Register a hook
+     *
+     * @method registerHook
+     * @param {Hook|String} hookOrName Hook instance or name
+     * @param {Function} handler Optional handler
+     * @param {Object} options Optional options
+     */
+    registerHook(hookOrName, handler = null, options = {}) {
+        this.hookService.registerHook(hookOrName, handler, options);
+    }
+ 
+    /**
+     * Execute hooks
+     *
+     * @method executeHook
+     * @param {String} hookName Hook name
+     * @param {...*} args Arguments to pass to hooks
+     * @returns {Promise<Array>} Array of hook results
+     */
+    async executeHook(hookName, ...args) {
+        return this.hookService.execute(hookName, ...args);
+    }
+ 
+    /**
+     * Get hooks
+     *
+     * @computed hooks
+     * @returns {Object} Hooks object
+     */
+    get hooks() {
+        return this.hookService.hooks;
+    }
+ 
+    // ============================================================================
+    // Utility Methods
+    // ============================================================================
+ 
+    /**
+     * Get view from transition
+     *
+     * @method getViewFromTransition
+     * @param {Object} transition Transition object
+     * @returns {String|null} View parameter
+     */
+    getViewFromTransition(transition) {
+        const queryParams = transition.to?.queryParams ?? { view: null };
+        return queryParams.view;
+    }
+ 
+    /**
+     * Virtual route redirect
+     * Handles redirecting to menu items based on URL slug
+     *
+     * @method virtualRouteRedirect
+     * @param {Object} transition Transition object
+     * @param {String} registryName Registry name
+     * @param {String} route Route name
+     * @param {Object} options Options
+     * @returns {Promise} Transition promise
+     */
+    async virtualRouteRedirect(transition, registryName, route, options = {}) {
+        const view = this.getViewFromTransition(transition);
+        const slug = window.location.pathname.replace('/', '');
+        const queryParams = this.urlSearchParams.all();
+        const menuItem = this.lookupMenuItemFromRegistry(registryName, slug, view);
+ 
+        if (menuItem && transition.from === null) {
+            return this.transitionMenuItem(route, menuItem, { queryParams }).then((transition) => {
+                if (options && options.restoreQueryParams === true) {
+                    this.urlSearchParams.setParamsToCurrentUrl(queryParams);
+                }
+                return transition;
+            });
+        }
+    }
+ 
+    /**
+     * Transition to a menu item
+     * Handles section, slug, and view parameters for virtual routes
+     *
+     * @method transitionMenuItem
+     * @param {String} route Route name
+     * @param {Object} menuItem Menu item object with slug, view, and optional section
+     * @param {Object} options
+     * @param {Object} options.queryParams Query params to carry into the transition
+     * @returns {Transition} The router transition
+     */
+    @action
+    transitionMenuItem(route, menuItem, options = {}) {
+        const { slug, view, section } = menuItem;
+        const params = section && slug ? [section, slug] : [slug];
+        // The menu item's own view wins over a `view` among the carried query params.
+        const queryParams = { ...options.queryParams, ...(view ? { view } : {}) };
+ 
+        if (Object.keys(queryParams).length) {
+            return this.router.transitionTo(route, ...params, { queryParams });
+        }
+ 
+        return this.router.transitionTo(route, ...params);
+    }
+ 
+    /**
+     * Register a boot callback
+     *
+     * @method onBoot
+     * @param {Function} callback Callback function
+     */
+    onBoot(callback) {
+        if (typeof callback === 'function') {
+            this.bootCallbacks.pushObject(callback);
+        }
+    }
+ 
+    /**
+     * Execute boot callbacks
+     *
+     * @method executeBootCallbacks
+     */
+    async executeBootCallbacks() {
+        for (const callback of this.bootCallbacks) {
+            try {
+                await callback(this);
+            } catch (error) {
+                console.error('Error executing boot callback:', error);
+            }
+        }
+ 
+        // Mark boot as complete
+        this.extensionManager.finishBoot();
+    }
+ 
+    // ============================================================================
+    // Backward Compatibility Methods
+    // ============================================================================
+ 
+    /**
+     * Get menu items from a registry
+     * Backward compatibility facade
+     *
+     * @method getMenuItemsFromRegistry
+     * @param {String} registryName Registry name
+     * @returns {Array} Menu items
+     */
+    getMenuItemsFromRegistry(registryName) {
+        return this.menuService.getMenuItems(registryName) || A([]);
+    }
+ 
+    /**
+     * Get menu panels from a registry
+     * Backward compatibility facade
+     *
+     * @method getMenuPanelsFromRegistry
+     * @param {String} registryName Registry name
+     * @returns {Array} Menu panels
+     */
+    getMenuPanelsFromRegistry(registryName) {
+        return this.menuService.getMenuPanels(registryName) || A([]);
+    }
+ 
+    /**
+     * Lookup a menu item from a registry
+     * Backward compatibility facade
+     *
+     * @method lookupMenuItemFromRegistry
+     * @param {String} registryName Registry name
+     * @param {String} slug Menu item slug
+     * @param {String} view Optional view
+     * @param {String} section Optional section
+     * @returns {Object|null} Menu item or null
+     */
+    lookupMenuItemFromRegistry(registryName, slug, view = null, section = null) {
+        const items = this.getMenuItemsFromRegistry(registryName);
+        return items.find((item) => {
+            const slugMatch = item.slug === slug;
+            const viewMatch = !view || item.view === view;
+            const sectionMatch = !section || item.section === section;
+            return slugMatch && viewMatch && sectionMatch;
+        });
+    }
+ 
+    /**
+     * Create a registry event
+     * Backward compatibility facade
+     *
+     * @method createRegistryEvent
+     * @param {String} registryName Registry name
+     * @param {String} eventName Event name
+     * @param {...*} args Event arguments
+     */
+    createRegistryEvent(registryName, eventName, ...args) {
+        this.trigger(`${registryName}:${eventName}`, ...args);
+    }
+ 
+    /**
+     * Register after boot callback
+     * Backward compatibility facade
+     *
+     * @method afterBoot
+     * @param {Function} callback Callback function
+     */
+    afterBoot(callback) {
+        this.extensionManager.afterBoot(callback);
+    }
+ 
+    /**
+     * Create a menu item (internal helper)
+     * Backward compatibility helper
+     *
+     * @method _createMenuItem
+     * @param {String} title Menu item title
+     * @param {String} route Menu item route
+     * @param {Object} options Menu item options
+     * @returns {Object} Menu item object
+     */
+    _createMenuItem(title, route = null, options = {}) {
+        const menuItem = new MenuItem(title, route);
+ 
+        if (options.icon) menuItem.withIcon(options.icon);
+        if (options.component) menuItem.withComponent(options.component);
+        if (options.slug) menuItem.withSlug(options.slug);
+        if (options.section) menuItem.inSection(options.section);
+        if (options.priority) menuItem.withPriority(options.priority);
+        if (options.type) menuItem.withType(options.type);
+        if (options.wrapperClass) menuItem.withWrapperClass(options.wrapperClass);
+        if (options.queryParams) menuItem.withQueryParams(options.queryParams);
+        if (options.onClick) menuItem.withOnClick(options.onClick);
+ 
+        return menuItem.toObject();
+    }
+ 
+    /**
+     * Register a renderable component for cross-engine rendering
+     * Facade method - delegates to RegistryService
+     *
+     * @method registerRenderableComponent
+     * @param {String} registryName Registry name (slot identifier)
+     * @param {Object|Class|Array} component ExtensionComponent definition, component class, or array of either
+     * @param {Object} options Optional configuration
+     *
+     * @example
+     * // ExtensionComponent definition with path (lazy loading)
+     * universe.registerRenderableComponent(
+     *     'fleet-ops:component:order:details',
+     *     new ExtensionComponent('@fleetbase/storefront-engine', 'storefront-order-summary')
+     * );
+     */
+    registerRenderableComponent(registryName, component, options = {}) {
+        return this.registryService.registerRenderableComponent(registryName, component, options);
+    }
+ 
+    /**
+     * Get renderable components from a registry
+     * Backward compatibility method - delegates to RegistryService
+     *
+     * @method getRenderableComponentsFromRegistry
+     * @param {String} registryName Registry name
+     * @returns {Array} Array of component definitions/classes
+     */
+    getRenderableComponentsFromRegistry(registryName) {
+        return this.registryService.getRenderableComponents(registryName);
+    }
+ 
+    /**
+     * Register a helper to the application container
+     * Makes the helper available globally to all engines and the host app
+     * Facade method - delegates to RegistryService
+     *
+     * @method registerHelper
+     * @param {String} helperName The helper name (e.g., 'calculate-delivery-fee')
+     * @param {Function|Class|TemplateHelper} helperClassOrTemplateHelper Helper function, class, or TemplateHelper instance
+     * @param {Object} options Registration options
+     * @returns {Promise<void>}
+     *
+     * @example
+     * // Direct function registration
+     * await universe.registerHelper('calculate-delivery-fee', calculateDeliveryFeeHelper);
+     *
+     * @example
+     * // Lazy loading from engine (ensures engine is loaded first)
+     * import TemplateHelper from '@fleetbase/ember-core/contracts/template-helper';
+     * await universe.registerHelper(
+     *     'calculate-delivery-fee',
+     *     new TemplateHelper('@fleetbase/storefront-engine', 'helpers/calculate-delivery-fee')
+     * );
+     */
+    async registerHelper(helperName, helperClassOrTemplateHelper, options = {}) {
+        return await this.registryService.registerHelper(helperName, helperClassOrTemplateHelper, options);
+    }
+ 
+    /**
+     * Legacy method for registering components in engines
+     * Maintained for backward compatibility
+     *
+     * @method registerComponentInEngine
+     * @param {String} engineName Engine name
+     * @param {*} componentClass Component class
+     * @param {Object} options Options
+     */
+    async registerComponentInEngine(engineName, componentClass, options = {}) {
+        const engineInstance = await this.ensureEngineLoaded(engineName);
+ 
+        if (engineInstance && componentClass && typeof componentClass.name === 'string') {
+            const dasherized = componentClass.name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
+            engineInstance.register(`component:${componentClass.name}`, componentClass);
+            engineInstance.register(`component:${dasherized}`, componentClass);
+ 
+            if (options.registerAs) {
+                engineInstance.register(`component:${options.registerAs}`, componentClass);
+            }
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/hook-service.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/hook-service.js.html new file mode 100644 index 0000000..1b38f0f --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/hook-service.js.html @@ -0,0 +1,1099 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/hook-service.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe hook-service.js

+
+ +
+ 0% + Statements + 0/84 +
+ + +
+ 0% + Branches + 0/60 +
+ + +
+ 0% + Functions + 0/16 +
+ + +
+ 0% + Lines + 0/77 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import Service, { inject as service } from '@ember/service';
+import { tracked } from '@glimmer/tracking';
+import { getOwner } from '@ember/application';
+import Hook from '../../contracts/hook';
+import HookRegistry from '../../contracts/hook-registry';
+ 
+/**
+ * HookService
+ *
+ * Manages application lifecycle hooks and custom event hooks.
+ * Allows extensions to inject logic at specific points in the application.
+ *
+ * @class HookService
+ * @extends Service
+ */
+export default class HookService extends Service {
+    @service universe;
+ 
+    /**
+     * Reference to the root Ember Application Instance.
+     * Used for registering components/services to the application container
+     * for cross-engine sharing.
+     */
+    @tracked applicationInstance = null;
+ 
+    #hookRegistry = null;
+ 
+    /**
+     * The shared hook registry, resolved on first use.
+     *
+     * This is deliberately lazy rather than resolved in the constructor:
+     * #getApplication prefers an explicitly set applicationInstance over the
+     * owner, and setApplicationInstance can only run after construction. Doing
+     * the lookup here means that preference is actually honoured.
+     *
+     * Read-only. It was a writable field before, but nothing in the addon, its
+     * tests or the console app ever assigned it, and replacing a memoized
+     * container lookup from outside is not a meaningful operation.
+     *
+     * @type {HookRegistry}
+     */
+    get hookRegistry() {
+        return this.#resolveHookRegistry();
+    }
+ 
+    /**
+     * Memoized resolution, kept out of the getter body so a property read is not
+     * itself an assignment.
+     *
+     * @private
+     * @returns {HookRegistry}
+     */
+    #resolveHookRegistry() {
+        if (!this.#hookRegistry) {
+            this.#hookRegistry = this.#initializeHookRegistry();
+        }
+ 
+        return this.#hookRegistry;
+    }
+ 
+    /**
+     * Set the application instance
+     *
+     * @method setApplicationInstance
+     * @param {Application} application The root application instance
+     */
+    setApplicationInstance(application) {
+        this.applicationInstance = application;
+    }
+ 
+    /**
+     * Initialize shared hook registry singleton
+     * Ensures all HookService instances share the same hooks
+     *
+     * @private
+     * @returns {HookRegistry}
+     */
+    #initializeHookRegistry() {
+        const registryKey = 'registry:hooks';
+        const application = this.#getApplication();
+ 
+        if (!application.hasRegistration(registryKey)) {
+            application.register(registryKey, new HookRegistry(), {
+                instantiate: false,
+            });
+        }
+ 
+        return application.resolveRegistration(registryKey);
+    }
+ 
+    /**
+     * Get the application instance
+     * Tries multiple fallback methods to find the root application
+     *
+     * @private
+     * @returns {Application}
+     */
+    #getApplication() {
+        // First priority use the universe application instance
+        if (this.universe.applicationInstance) {
+            return this.universe.applicationInstance;
+        }
+ 
+        // Second priority: use applicationInstance if set
+        if (this.applicationInstance) {
+            return this.applicationInstance;
+        }
+ 
+        // Third priority: try to get application from owner
+        const owner = getOwner(this);
+        if (owner && owner.application) {
+            return owner.application;
+        }
+ 
+        // Last resort: return owner itself (might be EngineInstance)
+        return owner;
+    }
+ 
+    /**
+     * Getter and setter for hooks property
+     * Delegates to the shared hookRegistry object
+     */
+    get hooks() {
+        return this.hookRegistry.hooks;
+    }
+ 
+    set hooks(value) {
+        this.hookRegistry.hooks = value;
+    }
+ 
+    /**
+     * Find a specific hook
+     *
+     * @private
+     * @method #findHook
+     * @param {String} hookName Hook name
+     * @param {String} hookId Hook ID
+     * @returns {Object|null} Hook or null
+     */
+    #findHook(hookName, hookId) {
+        const hookList = this.hooks[hookName] || [];
+        return hookList.find((h) => h.id === hookId) || null;
+    }
+ 
+    /**
+     * Normalize a hook input to a plain object
+     *
+     * @private
+     * @method #normalizeHook
+     * @param {Hook|String} input Hook instance or hook name
+     * @param {Function} handler Optional handler
+     * @param {Object} options Optional options
+     * @returns {Object} Normalized hook object
+     */
+    #normalizeHook(input, handler, options) {
+        if (input instanceof Hook) {
+            return input.toObject();
+        }
+ 
+        if (typeof input === 'string') {
+            const hook = new Hook(input, handler);
+ 
+            if (options.priority !== undefined) hook.withPriority(options.priority);
+            if (options.once) hook.once();
+            if (options.id) hook.withId(options.id);
+            if (options.enabled !== undefined) hook.setEnabled(options.enabled);
+ 
+            return hook.toObject();
+        }
+ 
+        return input;
+    }
+ 
+    /**
+     * Register a hook
+     *
+     * @method registerHook
+     * @param {Hook|String} hookOrName Hook instance or hook name
+     * @param {Function} handler Optional handler (if first param is string)
+     * @param {Object} options Optional options
+     */
+    registerHook(hookOrName, handler = null, options = {}) {
+        const hook = this.#normalizeHook(hookOrName, handler, options);
+ 
+        if (!this.hooks[hook.name]) {
+            this.hooks[hook.name] = [];
+        }
+ 
+        this.hooks[hook.name].push(hook);
+ 
+        // Sort by priority (lower numbers first)
+        this.hooks[hook.name].sort((a, b) => a.priority - b.priority);
+    }
+ 
+    /**
+     * Execute all hooks for a given name
+     *
+     * @method execute
+     * @param {String} hookName Hook name
+     * @param {...*} args Arguments to pass to hook handlers
+     * @returns {Promise<Array>} Array of hook results
+     */
+    async execute(hookName, ...args) {
+        const hookList = this.hooks[hookName] || [];
+        const results = [];
+ 
+        for (const hook of hookList) {
+            if (!hook.enabled) {
+                continue;
+            }
+ 
+            if (typeof hook.handler === 'function') {
+                try {
+                    const result = await hook.handler(...args);
+                    results.push(result);
+ 
+                    // Remove hook if it should only run once
+                    if (hook.once) {
+                        this.removeHook(hookName, hook.id);
+                    }
+                } catch (error) {
+                    console.error(`Error executing hook '${hookName}' (${hook.id}):`, error);
+                }
+            }
+        }
+ 
+        return results;
+    }
+ 
+    /**
+     * Execute hooks synchronously
+     *
+     * @method executeSync
+     * @param {String} hookName Hook name
+     * @param {...*} args Arguments to pass to hook handlers
+     * @returns {Array} Array of hook results
+     */
+    executeSync(hookName, ...args) {
+        const hookList = this.hooks[hookName] || [];
+        const results = [];
+ 
+        for (const hook of hookList) {
+            if (!hook.enabled) {
+                continue;
+            }
+ 
+            if (typeof hook.handler === 'function') {
+                try {
+                    const result = hook.handler(...args);
+                    results.push(result);
+ 
+                    if (hook.once) {
+                        this.removeHook(hookName, hook.id);
+                    }
+                } catch (error) {
+                    console.error(`Error executing hook '${hookName}' (${hook.id}):`, error);
+                }
+            }
+        }
+ 
+        return results;
+    }
+ 
+    /**
+     * Remove a specific hook
+     *
+     * @method removeHook
+     * @param {String} hookName Hook name
+     * @param {String} hookId Hook ID
+     */
+    removeHook(hookName, hookId) {
+        if (this.hooks[hookName]) {
+            this.hooks[hookName] = this.hooks[hookName].filter((h) => h.id !== hookId);
+        }
+    }
+ 
+    /**
+     * Remove all hooks for a given name
+     *
+     * @method removeAllHooks
+     * @param {String} hookName Hook name
+     */
+    removeAllHooks(hookName) {
+        if (this.hooks[hookName]) {
+            this.hooks[hookName] = [];
+        }
+    }
+ 
+    /**
+     * Get all hooks for a given name
+     *
+     * @method getHooks
+     * @param {String} hookName Hook name
+     * @returns {Array} Array of hooks
+     */
+    getHooks(hookName) {
+        return this.hooks[hookName] || [];
+    }
+ 
+    /**
+     * Check if a hook exists
+     *
+     * @method hasHook
+     * @param {String} hookName Hook name
+     * @returns {Boolean} True if hook exists
+     */
+    hasHook(hookName) {
+        return this.hooks[hookName] && this.hooks[hookName].length > 0;
+    }
+ 
+    /**
+     * Enable a hook
+     *
+     * @method enableHook
+     * @param {String} hookName Hook name
+     * @param {String} hookId Hook ID
+     */
+    enableHook(hookName, hookId) {
+        const hook = this.#findHook(hookName, hookId);
+        if (hook) {
+            hook.enabled = true;
+        }
+    }
+ 
+    /**
+     * Disable a hook
+     *
+     * @method disableHook
+     * @param {String} hookName Hook name
+     * @param {String} hookId Hook ID
+     */
+    disableHook(hookName, hookId) {
+        const hook = this.#findHook(hookName, hookId);
+        if (hook) {
+            hook.enabled = false;
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/index.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/index.html new file mode 100644 index 0000000..ff67650 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/index.html @@ -0,0 +1,176 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe + + + + + + + + + +
+
+

All files ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe

+
+ +
+ 0% + Statements + 0/489 +
+ + +
+ 0% + Branches + 0/442 +
+ + +
+ 0% + Functions + 0/135 +
+ + +
+ 0% + Lines + 0/458 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
hook-service.js +
+
0%0/840%0/600%0/160%0/77
menu-service.js +
+
0%0/1370%0/1610%0/360%0/120
registry-service.js +
+
0%0/1190%0/1050%0/350%0/116
resource-view-service.js +
+
0%0/850%0/680%0/240%0/83
widget-service.js +
+
0%0/640%0/480%0/240%0/62
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/menu-service.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/menu-service.js.html new file mode 100644 index 0000000..713a4d8 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/menu-service.js.html @@ -0,0 +1,1951 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/menu-service.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe menu-service.js

+
+ +
+ 0% + Statements + 0/137 +
+ + +
+ 0% + Branches + 0/161 +
+ + +
+ 0% + Functions + 0/36 +
+ + +
+ 0% + Lines + 0/120 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import Service from '@ember/service';
+import Evented from '@ember/object/evented';
+import { tracked } from '@glimmer/tracking';
+import { inject as service } from '@ember/service';
+import { dasherize } from '@ember/string';
+import { A, isArray } from '@ember/array';
+import MenuItem from '../../contracts/menu-item';
+import MenuPanel from '../../contracts/menu-panel';
+ 
+/**
+ * MenuManagerService
+ *
+ * Manages all menu items and panels in the application.
+ * Uses RegistryService for storage, providing cross-engine access.
+ *
+ * @class MenuService
+ * @extends Service
+ */
+export default class MenuService extends Service.extend(Evented) {
+    @service('universe/registry-service') registry;
+    @service universe;
+ 
+    /**
+     * Reference to the root Ember Application Instance.
+     * Used for registering components/services to the application container
+     * for cross-engine sharing.
+     */
+    @tracked applicationInstance = null;
+ 
+    /**
+     * Set the application instance (for consistency with other services)
+     *
+     * @method setApplicationInstance
+     * @param {Application} application The root application instance
+     */
+    setApplicationInstance(application) {
+        this.applicationInstance = application;
+    }
+ 
+    /**
+     * Wrap an onClick handler to automatically pass menuItem and universe as parameters
+     *
+     * @private
+     * @method #wrapOnClickHandler
+     * @param {Function} onClick The original onClick function
+     * @param {Object} menuItem The menu item object
+     * @returns {Function} Wrapped onClick function
+     */
+    #wrapOnClickHandler(onClick, menuItem) {
+        const universe = this.universe;
+        return function () {
+            return onClick(menuItem, universe);
+        };
+    }
+ 
+    /**
+     * Normalize a menu item input to a plain object
+     *
+     * @private
+     * @method #normalizeMenuItem
+     * @param {MenuItem|String|Object} input MenuItem instance, title, or object
+     * @param {String} route Optional route
+     * @param {Object} options Optional options
+     * @returns {Object} Normalized menu item object
+     */
+    #normalizeMenuItem(input, route = null, options = {}) {
+        let menuItemObj;
+ 
+        if (input instanceof MenuItem) {
+            menuItemObj = input.toObject();
+        } else if (typeof input === 'object' && input !== null && !input.title) {
+            menuItemObj = input;
+        } else if (typeof input === 'string') {
+            const menuItem = new MenuItem(input, route);
+ 
+            // Apply options
+            Object.keys(options).forEach((key) => {
+                if (key === 'icon') menuItem.withIcon(options[key]);
+                else if (key === 'priority') menuItem.withPriority(options[key]);
+                else if (key === 'component') menuItem.withComponent(options[key]);
+                else if (key === 'slug') menuItem.withSlug(options[key]);
+                else if (key === 'section') menuItem.inSection(options[key]);
+                else if (key === 'index') menuItem.atIndex(options[key]);
+                else if (key === 'type') menuItem.withType(options[key]);
+                else if (key === 'wrapperClass') menuItem.withWrapperClass(options[key]);
+                else if (key === 'queryParams') menuItem.withQueryParams(options[key]);
+                else if (key === 'onClick') menuItem.withOnClick(options[key]);
+                else menuItem.setOption(key, options[key]);
+            });
+ 
+            menuItemObj = menuItem.toObject();
+        } else {
+            menuItemObj = input;
+        }
+ 
+        // Wrap onClick handler to automatically pass menuItem and universe
+        if (menuItemObj && typeof menuItemObj.onClick === 'function') {
+            menuItemObj.onClick = this.#wrapOnClickHandler(menuItemObj.onClick, menuItemObj);
+        }
+ 
+        return menuItemObj;
+    }
+ 
+    /**
+     * Normalize a menu panel input to a plain object
+     *
+     * @private
+     * @method #normalizeMenuPanel
+     * @param {MenuPanel|String|Object} input MenuPanel instance, title, or object
+     * @param {Array} items Optional items
+     * @param {Object} options Optional options
+     * @returns {Object} Normalized menu panel object
+     */
+    #normalizeMenuPanel(input, items, options) {
+        if (input instanceof MenuPanel) {
+            return input.toObject();
+        }
+ 
+        if (typeof input === 'object' && input !== null && !input.title) {
+            return input;
+        }
+ 
+        if (typeof input === 'string') {
+            const panel = new MenuPanel(input, items);
+ 
+            if (options.slug) panel.withSlug(options.slug);
+            if (options.icon) panel.withIcon(options.icon);
+            if (options.priority) panel.withPriority(options.priority);
+ 
+            return panel.toObject();
+        }
+ 
+        return input;
+    }
+ 
+    // ============================================================================
+    // Registration Methods
+    // ============================================================================
+ 
+    /**
+     * Register a header menu item
+     *
+     * @method registerHeaderMenuItem
+     * @param {MenuItem|String} menuItemOrTitle MenuItem instance or title
+     * @param {String} route Optional route (if first param is string)
+     * @param {Object} options Optional options (if first param is string)
+     */
+    registerHeaderMenuItem(itemOrTitle, route = null, options = {}) {
+        const menuItem = this.#normalizeMenuItem(itemOrTitle, route, options);
+        this.registry.register('header', 'menu-item', menuItem.slug, menuItem);
+ 
+        // Auto-register each shortcut as a first-class header menu item so that
+        // they appear in the customiser's "All Extensions" list and can be found
+        // by id in allItems when pinned to the bar.
+        if (isArray(menuItem.shortcuts)) {
+            for (const sc of menuItem.shortcuts) {
+                const scId = sc.id ?? dasherize(menuItem.id + '-sc-' + sc.title);
+                const scSlug = sc.slug ?? scId;
+ 
+                // Build a first-class item that supports the full MenuItem
+                // property surface.  Each property falls back to the parent's
+                // value so shortcuts inherit sensible defaults without the
+                // consumer having to repeat them.
+                const scItem = {
+                    // ── Identity ──────────────────────────────────────────────
+                    id: scId,
+                    slug: scSlug,
+                    title: sc.title,
+                    text: sc.text ?? sc.title,
+                    label: sc.label ?? sc.title,
+                    view: sc.view ?? scId,
+ 
+                    // ── Routing ───────────────────────────────────────────────
+                    route: sc.route ?? menuItem.route,
+                    section: sc.section ?? null,
+                    queryParams: sc.queryParams ?? {},
+                    routeParams: sc.routeParams ?? [],
+ 
+                    // ── Icons (full surface) ──────────────────────────────────
+                    icon: sc.icon ?? menuItem.icon,
+                    iconPrefix: sc.iconPrefix ?? menuItem.iconPrefix,
+                    iconSize: sc.iconSize ?? menuItem.iconSize ?? null,
+                    iconClass: sc.iconClass ?? menuItem.iconClass ?? null,
+                    iconComponent: sc.iconComponent ?? null,
+                    iconComponentOptions: sc.iconComponentOptions ?? {},
+ 
+                    // ── Metadata ──────────────────────────────────────────────
+                    description: sc.description ?? null,
+                    // Shortcuts inherit parent tags so they surface under the
+                    // same search terms; shortcut-specific tags take precedence.
+                    tags: isArray(sc.tags) ? sc.tags : isArray(menuItem.tags) ? menuItem.tags : null,
+ 
+                    // ── Behaviour ─────────────────────────────────────────────
+                    onClick: sc.onClick ?? null,
+                    disabled: sc.disabled ?? false,
+                    // Shortcuts without their own permission inherit the parent's,
+                    // so the header never offers a module the user cannot open.
+                    permission: sc.permission ?? menuItem.permission ?? null,
+                    type: sc.type ?? 'default',
+                    buttonType: sc.buttonType ?? null,
+ 
+                    // ── Styling ───────────────────────────────────────────────
+                    class: sc.class ?? null,
+                    inlineClass: sc.inlineClass ?? null,
+                    wrapperClass: sc.wrapperClass ?? null,
+ 
+                    // ── Internal flags ────────────────────────────────────────
+                    _isShortcut: true,
+                    _parentTitle: menuItem.title,
+                    _parentId: menuItem.id,
+                    priority: (menuItem.priority ?? 0) + 1,
+                    _isMenuItem: true,
+                };
+                this.registry.register('header', 'menu-item', scSlug, scItem);
+                this.trigger('menuItem.registered', scItem, 'header');
+            }
+        }
+ 
+        // Trigger event for backward compatibility
+        this.trigger('menuItem.registered', menuItem, 'header');
+    }
+ 
+    /**
+     * Register an admin menu item
+     *
+     * @method registerAdminMenuItem
+     * @param {MenuItem|String} itemOrTitle MenuItem instance or title
+     * @param {String} route Optional route (if first param is string)
+     * @param {Object} options Optional options (if first param is string)
+     */
+    registerAdminMenuItem(itemOrTitle, route = null, options = {}) {
+        const menuItem = this.#normalizeMenuItem(itemOrTitle, route, options);
+        this.registry.register('console:admin', 'menu-item', menuItem.slug, menuItem);
+ 
+        // Trigger event for backward compatibility
+        this.trigger('menuItem.registered', menuItem, 'console:admin');
+    }
+ 
+    /**
+     * Register an organization menu item
+     *
+     * @method registerOrganizationMenuItem
+     * @param {MenuItem|String} menuItemOrTitle MenuItem instance or title
+     * @param {Object} options Optional options
+     */
+    registerOrganizationMenuItem(menuItemOrTitle, options = {}) {
+        const menuItem = this.#normalizeMenuItem(menuItemOrTitle, options.route || 'console.virtual', options);
+ 
+        if (!menuItem.section) {
+            menuItem.section = 'settings';
+        }
+ 
+        this.registry.register('console:account', 'menu-item', `organization:${menuItem.slug}`, menuItem);
+    }
+ 
+    /**
+     * Register a user menu item
+     *
+     * @method registerUserMenuItem
+     * @param {MenuItem|String} menuItemOrTitle MenuItem instance or title
+     * @param {Object} options Optional options
+     */
+    registerUserMenuItem(menuItemOrTitle, options = {}) {
+        const menuItem = this.#normalizeMenuItem(menuItemOrTitle, options.route || 'console.virtual', options);
+ 
+        if (!menuItem.section) {
+            menuItem.section = 'account';
+        }
+ 
+        this.registry.register('console:account', 'menu-item', `user:${menuItem.slug}`, menuItem);
+    }
+ 
+    /**
+     * Register an admin menu panel
+     *
+     * @method registerAdminMenuPanel
+     * @param {MenuPanel|String} panelOrTitle MenuPanel instance or title
+     * @param {Array} items Optional items array (if first param is string)
+     * @param {Object} options Optional options (if first param is string)
+     */
+    registerAdminMenuPanel(panelOrTitle, items = [], options = {}) {
+        const panel = this.#normalizeMenuPanel(panelOrTitle, items, options);
+        this.registry.register('console:admin', 'menu-panel', panel.slug, panel);
+ 
+        // The PDF states: "Additionally registering menu panels should also register there items."
+        // We assume the items are passed in the panel object or items array.
+        if (panel.items && panel.items.length) {
+            panel.items = panel.items.map((item) => {
+                const menuItem = this.#normalizeMenuItem(item);
+ 
+                // CRITICAL: Original behavior for panel items:
+                // - slug = panel slug (e.g., 'fleet-ops') ← Used in URL
+                // - view = item slug (e.g., 'navigator-app') ← Used in query param
+                // - section = null (not used for panel items)
+                // Result: /admin/fleet-ops?view=navigator-app
+ 
+                const itemSlug = menuItem.slug; // Save the original item slug
+                menuItem.slug = panel.slug; // Set slug to panel slug for URL
+                menuItem.view = itemSlug; // Set view to item slug for query param
+                menuItem.section = null; // Panel items don't use section
+ 
+                // Mark as panel item to prevent duplication in main menu
+                menuItem._isPanelItem = true;
+                menuItem._panelSlug = panel.slug;
+ 
+                // Register with the item slug as key (for lookup)
+                this.registry.register('console:admin', 'menu-item', itemSlug, menuItem);
+ 
+                // Trigger event for backward compatibility
+                this.trigger('menuItem.registered', menuItem, 'console:admin');
+ 
+                // Return the modified menu item so panel.items gets updated
+                return menuItem;
+            });
+        }
+ 
+        // Trigger event for backward compatibility
+        this.trigger('menuPanel.registered', panel, 'console:admin');
+    }
+ 
+    /**
+     * Register a settings menu item
+     *
+     * @method registerSettingsMenuItem
+     * @param {MenuItem|String} menuItemOrTitle MenuItem instance or title
+     * @param {Object} options Optional options
+     */
+    registerSettingsMenuItem(menuItemOrTitle, options = {}) {
+        const menuItem = this.#normalizeMenuItem(menuItemOrTitle, options.route || 'console.settings.virtual', options);
+ 
+        this.registry.register('console:settings', 'menu-item', menuItem.slug, menuItem);
+    }
+ 
+    /**
+     * Register a menu item to a custom registry
+     *
+     * Supports two patterns:
+     * 1. Original: registerMenuItem(registryName, title, options)
+     * 2. New: registerMenuItem(registryName, menuItemInstance)
+     *
+     * @method registerMenuItem
+     * @param {String} registryName Registry name (e.g., 'auth:login', 'engine:fleet-ops')
+     * @param {String|MenuItem} titleOrMenuItem Menu item title string or MenuItem instance
+     * @param {Object} options Optional options (only used with title string)
+     */
+    registerMenuItem(registryName, titleOrMenuItem, options = {}) {
+        let menuItem;
+ 
+        // Normalize the menu item first (handles both MenuItem instances and string titles)
+        if (titleOrMenuItem instanceof MenuItem) {
+            menuItem = this.#normalizeMenuItem(titleOrMenuItem);
+        } else {
+            // Original pattern: title string + options
+            const title = titleOrMenuItem;
+            const route = options.route || `console.${dasherize(registryName)}.virtual`;
+ 
+            // Set defaults matching original behavior
+            const slug = options.slug || '~';
+ 
+            menuItem = this.#normalizeMenuItem(title, route, {
+                ...options,
+                slug,
+            });
+        }
+ 
+        // Apply finalView normalization consistently for ALL menu items
+        // If slug === view, set view to null to prevent redundant query params
+        // This matches the legacy behavior: const finalView = (slug === view) ? null : view;
+        if (menuItem.slug && menuItem.view && menuItem.slug === menuItem.view) {
+            menuItem.view = null;
+        }
+ 
+        // Register the menu item. Items registered by title share the '~' slug (their URL is
+        // `virtual/~?view=<view>`), so they are keyed by view or title instead; keyed by '~'
+        // each would replace the one before it.
+        const key = menuItem.slug && menuItem.slug !== '~' ? menuItem.slug : (menuItem.view ?? menuItem.title);
+        this.registry.register(registryName, 'menu-item', key, menuItem);
+ 
+        // Trigger event
+        this.trigger('menuItem.registered', menuItem, registryName);
+    }
+ 
+    // ============================================================================
+    // Getter Methods (Improved DX)
+    // ============================================================================
+ 
+    /**
+     * Get menu items from a registry
+     *
+     * @method getMenuItems
+     * @param {String} registryName Registry name (e.g., 'engine:fleet-ops')
+     * @returns {Array} Menu items
+     */
+    getMenuItems(registryName) {
+        return this.registry.getRegistry(registryName, 'menu-item');
+    }
+ 
+    /**
+     * Get menu panels from a registry
+     *
+     * @method getMenuPanels
+     * @param {String} registryName Registry name (e.g., 'engine:fleet-ops')
+     * @returns {Array} Menu panels
+     */
+    getMenuPanels(registryName) {
+        return this.registry.getRegistry(registryName, 'menu-panel');
+    }
+ 
+    /**
+     * Lookup a menu item from a registry
+     *
+     * @method lookupMenuItem
+     * @param {String} registryName Registry name
+     * @param {String} slug Menu item slug
+     * @param {String} view Optional view
+     * @param {String} section Optional section
+     * @returns {Object|null} Menu item or null
+     */
+    lookupMenuItem(registryName, slug, view = null, section = null) {
+        const items = this.getMenuItems(registryName);
+        return items.find((item) => {
+            const slugMatch = item.slug === slug;
+            const viewMatch = !view || item.view === view;
+            const sectionMatch = !section || item.section === section;
+            return slugMatch && viewMatch && sectionMatch;
+        });
+    }
+ 
+    /**
+     * Alias for lookupMenuItem
+     *
+     * @method getMenuItem
+     * @param {String} registryName Registry name
+     * @param {String} slug Menu item slug
+     * @param {String} view Optional view
+     * @param {String} section Optional section
+     * @returns {Object|null} Menu item or null
+     */
+    getMenuItem(registryName, slug, view = null, section = null) {
+        return this.lookupMenuItem(registryName, slug, view, section);
+    }
+ 
+    /**
+     * Get header menu items
+     *
+     * @method getHeaderMenuItems
+     * @returns {Array} Header menu items sorted by priority
+     */
+    getHeaderMenuItems() {
+        const items = this.registry.getRegistry('header', 'menu-item');
+        // Sort extensions by priority first, then append shortcuts at the end.
+        // Shortcuts must never interleave with extensions in the sorted list
+        // because the default bar is built by slicing the first N items — if
+        // shortcuts sort between extensions (e.g. priority 1.1 between 1 and 2)
+        // they would displace real extensions from the default pinned bar.
+        // A(...).filter() returns a plain array, which has no sortBy, so the
+        // result has to be re-wrapped before sorting.
+        const extensions = A(A(items).filter((i) => !i._isShortcut)).sortBy('priority');
+        const shortcuts = A(items).filter((i) => i._isShortcut);
+        return A([...extensions, ...shortcuts]);
+    }
+ 
+    /**
+     * Get organization menu items
+     *
+     * @method getOrganizationMenuItems
+     * @returns {Array} Organization menu items
+     */
+    getOrganizationMenuItems() {
+        return this.registry.getAllFromPrefix('console:account', 'menu-item', 'organization:');
+    }
+ 
+    /**
+     * Get user menu items
+     *
+     * @method getUserMenuItems
+     * @returns {Array} User menu items
+     */
+    getUserMenuItems() {
+        return this.registry.getAllFromPrefix('console:account', 'menu-item', 'user:');
+    }
+ 
+    /**
+     * Get admin menu panels
+     *
+     * @method getAdminMenuPanels
+     * @returns {Array} Admin panels sorted by priority
+     */
+    getAdminMenuPanels() {
+        const panels = this.registry.getRegistry('console:admin', 'menu-panel');
+        return A(panels).sortBy('priority');
+    }
+ 
+    /**
+     * Alias for getAdminMenuPanels
+     *
+     * @method getAdminPanels
+     * @returns {Array} Admin panels
+     */
+    getAdminPanels() {
+        return this.getAdminMenuPanels();
+    }
+ 
+    /**
+     * Get admin menu items
+     * Excludes items that belong to panels (to prevent duplication)
+     *
+     * @method getAdminMenuItems
+     * @returns {Array} Admin menu items (excluding panel items)
+     */
+    getAdminMenuItems() {
+        const items = this.registry.getRegistry('console:admin', 'menu-item');
+        // Filter out panel items to prevent duplication in the UI
+        return items.filter((item) => !item._isPanelItem);
+    }
+ 
+    /**
+     * Get menu items from a specific panel
+     *
+     * @method getMenuItemsFromPanel
+     * @param {String} panelSlug Panel slug
+     * @returns {Array} Menu items belonging to the panel
+     */
+    getMenuItemsFromPanel(panelSlug) {
+        const items = this.registry.getRegistry('console:admin', 'menu-item');
+        return items.filter((item) => item._panelSlug === panelSlug);
+    }
+ 
+    /**
+     * Get settings menu items
+     *
+     * @method getSettingsMenuItems
+     * @returns {Array} Settings menu items
+     */
+    getSettingsMenuItems() {
+        return this.registry.getRegistry('console:settings', 'menu-item');
+    }
+ 
+    /**
+     * Get settings menu panels
+     *
+     * @method getSettingsMenuPanels
+     * @returns {Array} Settings menu panels
+     */
+    getSettingsMenuPanels() {
+        const panels = this.registry.getRegistry('console:settings', 'menu-panel');
+        return A(panels).sortBy('priority');
+    }
+    // ============================================================================
+    // Computed Getters (for template access)
+    // ============================================================================
+ 
+    /**
+     * Get header menu items (computed getter)
+     *
+     * @computed headerMenuItems
+     * @returns {Array} Header menu items
+     */
+    get headerMenuItems() {
+        return this.getHeaderMenuItems();
+    }
+ 
+    /**
+     * Get organization menu items (computed getter)
+     *
+     * @computed organizationMenuItems
+     * @returns {Array} Organization menu items
+     */
+    get organizationMenuItems() {
+        return this.getOrganizationMenuItems();
+    }
+ 
+    /**
+     * Get user menu items (computed getter)
+     *
+     * @computed userMenuItems
+     * @returns {Array} User menu items
+     */
+    get userMenuItems() {
+        return this.getUserMenuItems();
+    }
+ 
+    /**
+     * Get admin menu items (computed getter)
+     *
+     * @computed adminMenuItems
+     * @returns {Array} Admin menu items
+     */
+    get adminMenuItems() {
+        return this.getAdminMenuItems();
+    }
+ 
+    /**
+     * Get admin menu panels (computed getter)
+     *
+     * @computed adminMenuPanels
+     * @returns {Array} Admin menu panels
+     */
+    get adminMenuPanels() {
+        return this.getAdminMenuPanels();
+    }
+ 
+    /**
+     * Get settings menu items (computed getter)
+     *
+     * @computed settingsMenuItems
+     * @returns {Array} Settings menu items
+     */
+    get settingsMenuItems() {
+        return this.getSettingsMenuItems();
+    }
+ 
+    /**
+     * Get settings menu panels (computed getter)
+     *
+     * @computed settingsMenuPanels
+     * @returns {Array} Settings menu panels
+     */
+    get settingsMenuPanels() {
+        return this.getSettingsMenuPanels();
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/registry-service.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/registry-service.js.html new file mode 100644 index 0000000..3ac6859 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/registry-service.js.html @@ -0,0 +1,1924 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/registry-service.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe registry-service.js

+
+ +
+ 0% + Statements + 0/119 +
+ + +
+ 0% + Branches + 0/105 +
+ + +
+ 0% + Functions + 0/35 +
+ + +
+ 0% + Lines + 0/116 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import Service, { inject as service } from '@ember/service';
+import { tracked } from '@glimmer/tracking';
+import { warn } from '@ember/debug';
+import { A, isArray } from '@ember/array';
+import { TrackedObject } from 'tracked-built-ins';
+import { getOwner } from '@ember/application';
+import TemplateHelper from '../../contracts/template-helper';
+import UniverseRegistry from '../../contracts/universe-registry';
+ 
+/**
+ * RegistryService
+ *
+ * Fully dynamic, Map-based registry for storing categorized items.
+ * Supports grouped registries with multiple list types per section.
+ *
+ * Structure:
+ * registries (TrackedMap) → section name → TrackedObject { list-name: A([]), ... }
+ *
+ * Usage:
+ * ```javascript
+ * // Register an item to a specific list within a section
+ * registryService.register('console:admin', 'menu-panels', 'fleet-ops', panelObject);
+ *
+ * // Get all items from a list
+ * const panels = registryService.getRegistry('console:admin', 'menu-panels');
+ *
+ * // Lookup specific item
+ * const panel = registryService.lookup('console:admin', 'menu-panels', 'fleet-ops');
+ * ```
+ *
+ * @class RegistryService
+ * @extends Service
+ */
+/**
+ * Whether a helper is a class to instantiate (a `Helper` subclass) rather than a function.
+ */
+function isHelperClass(value) {
+    return typeof value.prototype?.compute === 'function';
+}
+ 
+/**
+ * Whether a registry item can carry a `_registryKey`: any object, or a class.
+ */
+function isKeyable(value) {
+    return (typeof value === 'object' && value !== null) || typeof value === 'function';
+}
+ 
+export default class RegistryService extends Service {
+    @service('universe/extension-manager') extensionManager;
+ 
+    /**
+     * Reference to the root Ember Application Instance.
+     * Used for registering components/services to the application container
+     * for cross-engine sharing.
+     */
+    @tracked applicationInstance = null;
+ 
+    #registry = null;
+ 
+    /**
+     * The singleton UniverseRegistry instance, resolved on first use.
+     * Initialized once and shared across the app and all engines.
+     *
+     * This is deliberately lazy. It was a field initializer, which runs during
+     * construction — before setApplicationInstance can possibly have been
+     * called — so #initializeRegistry's documented first choice, an explicitly
+     * set applicationInstance, could never be taken and every service fell back
+     * to the owner. HookService's registry was made lazy for the same reason.
+     *
+     * @type {UniverseRegistry}
+     */
+    get registry() {
+        return this.#resolveRegistry();
+    }
+ 
+    /**
+     * Memoized resolution, kept out of the getter body so a property read is not
+     * itself an assignment.
+     *
+     * @private
+     * @returns {UniverseRegistry}
+     */
+    #resolveRegistry() {
+        if (!this.#registry) {
+            this.#registry = this.#initializeRegistry();
+        }
+ 
+        return this.#registry;
+    }
+ 
+    /**
+     * Getter for the registries TrackedMap.
+     * Provides access to the shared registry data.
+     * @type {TrackedMap<string, TrackedObject>}
+     */
+    get registries() {
+        return this.registry.registries;
+    }
+ 
+    /**
+     * Sets the root Ember Application Instance.
+     * Called by an initializer to enable cross-engine registration.
+     * @method setApplicationInstance
+     * @param {Object} appInstance
+     */
+    setApplicationInstance(application) {
+        this.applicationInstance = application;
+    }
+ 
+    /**
+     * Initializes the UniverseRegistry singleton.
+     * Registers it to the application container if not already registered.
+     * This ensures all service instances (app and engines) share the same registry.
+     * @private
+     * @method #initializeRegistry
+     * @returns {UniverseRegistry} The singleton registry instance
+     */
+    #initializeRegistry() {
+        const registryKey = 'registry:universe';
+ 
+        // First priority: use applicationInstance if set
+        let application = this.applicationInstance;
+ 
+        if (!application) {
+            // Second priority: try to get from owner
+            const owner = getOwner(this);
+            if (owner && owner.application) {
+                application = owner.application;
+            } else {
+                warn('[RegistryService] Could not find application instance for registry initialization', {
+                    id: 'registry-service.no-application',
+                });
+                // Return a new instance as fallback (won't be shared)
+                return new UniverseRegistry();
+            }
+        }
+ 
+        // Register the singleton if not already registered
+        if (!application.hasRegistration(registryKey)) {
+            application.register(registryKey, new UniverseRegistry(), {
+                instantiate: false,
+            });
+        }
+ 
+        // Resolve and return the singleton instance
+        return application.resolveRegistration(registryKey);
+    }
+ 
+    /**
+     * Get or create a registry section.
+     * Returns a TrackedObject containing dynamic lists.
+     *
+     * @method getOrCreateSection
+     * @param {String} sectionName Section name (e.g., 'console:admin', 'dashboard:widgets')
+     * @returns {TrackedObject} The section object
+     */
+    getOrCreateSection(sectionName) {
+        if (!this.registries.has(sectionName)) {
+            this.registries.set(sectionName, new TrackedObject({}));
+        }
+        return this.registries.get(sectionName);
+    }
+ 
+    /**
+     * Get or create a list within a section.
+     * Returns an Ember Array for the specified list.
+     *
+     * @method getOrCreateList
+     * @param {String} sectionName Section name
+     * @param {String} listName List name (e.g., 'menu-items', 'menu-panels')
+     * @returns {Array} The Ember Array for the list
+     */
+    getOrCreateList(sectionName, listName) {
+        const section = this.getOrCreateSection(sectionName);
+ 
+        if (!section[listName]) {
+            section[listName] = A([]);
+        }
+ 
+        return section[listName];
+    }
+ 
+    /**
+     * Register an item in a specific list within a registry section.
+     *
+     * @method register
+     * @param {String} sectionName Section name (e.g., 'console:admin')
+     * @param {String} listName List name within the section (e.g., 'menu-items', 'menu-panels')
+     * @param {String} key Unique identifier for the item
+     * @param {Object} value The item to register
+     */
+    register(sectionName, listName, key, value) {
+        const registry = this.getOrCreateList(sectionName, listName);
+ 
+        // Store the key with the value for lookups. A class (a renderable component) is
+        // keyed too, or nothing could look it up by name.
+        if (isKeyable(value)) {
+            value._registryKey = key;
+        }
+ 
+        // Check if already exists
+        const existing = registry.find((item) => {
+            if (isKeyable(item)) {
+                return item._registryKey === key || item.slug === key || item.id === key || item.widgetId === key;
+            }
+            return false;
+        });
+ 
+        if (existing) {
+            // Update existing item
+            const index = registry.indexOf(existing);
+            registry.replace(index, 1, [value]);
+        } else {
+            // Add new item
+            registry.pushObject(value);
+        }
+    }
+ 
+    /**
+     * Get all items from a specific list within a registry section.
+     *
+     * @method getRegistry
+     * @param {String} sectionName Section name (e.g., 'console:admin')
+     * @param {String} listName List name within the section (e.g., 'menu-items', 'menu-panels')
+     * @returns {Array} Array of items in the list
+     */
+    getRegistry(sectionName, listName) {
+        const section = this.registries.get(sectionName);
+ 
+        if (!section || !section[listName]) {
+            return A([]);
+        }
+ 
+        return section[listName];
+    }
+ 
+    /**
+     * Get the entire section object (all lists within a section).
+     *
+     * @method getSection
+     * @param {String} sectionName Section name
+     * @returns {TrackedObject|null} The section object or null
+     */
+    getSection(sectionName) {
+        return this.registries.get(sectionName) || null;
+    }
+ 
+    /**
+     * Lookup a specific item by key
+     *
+     * @method lookup
+     * @param {String} sectionName Section name (e.g., 'console:admin')
+     * @param {String} listName List name within the section (e.g., 'menu-items', 'menu-panels')
+     * @param {String} key Item key
+     * @returns {Object|null} The item or null if not found
+     */
+    lookup(sectionName, listName, key) {
+        const registry = this.getRegistry(sectionName, listName);
+        return (
+            registry.find((item) => {
+                if (isKeyable(item)) {
+                    return item._registryKey === key || item.slug === key || item.id === key || item.widgetId === key;
+                }
+                return false;
+            }) || null
+        );
+    }
+ 
+    /**
+     * Get items matching a key prefix
+     *
+     * @method getAllFromPrefix
+     * @param {String} sectionName Section name (e.g., 'console:admin')
+     * @param {String} listName List name within the section (e.g., 'menu-items')
+     * @param {String} prefix Key prefix to match
+     * @returns {Array} Matching items
+     */
+    getAllFromPrefix(sectionName, listName, prefix) {
+        const registry = this.getRegistry(sectionName, listName);
+        return registry.filter((item) => {
+            if (typeof item === 'object' && item !== null && item._registryKey) {
+                return item._registryKey.startsWith(prefix);
+            }
+            return false;
+        });
+    }
+ 
+    /**
+     * Register a renderable component for cross-engine rendering
+     * Supports both ExtensionComponent definitions and raw component classes
+     *
+     * @method registerRenderableComponent
+     * @param {String} registryName Registry name (slot identifier)
+     * @param {Object|Class|Array} component ExtensionComponent definition, component class, or array of either
+     * @param {Object} options Optional configuration
+     * @param {String} options.engineName Engine name (required for raw component classes)
+     *
+     * @example
+     * // ExtensionComponent definition with path (lazy loading)
+     * registryService.registerRenderableComponent(
+     *     'fleet-ops:component:order:details',
+     *     new ExtensionComponent('@fleetbase/storefront-engine', 'storefront-order-summary')
+     * );
+     *
+     * @example
+     * // ExtensionComponent definition with class (immediate)
+     * import MyComponent from './components/my-component';
+     * registryService.registerRenderableComponent(
+     *     'fleet-ops:component:order:details',
+     *     new ExtensionComponent('@fleetbase/fleetops-engine', MyComponent)
+     * );
+     *
+     * @example
+     * // Raw component class (requires engineName in options)
+     * registryService.registerRenderableComponent(
+     *     'fleet-ops:component:order:details',
+     *     MyComponent,
+     *     { engineName: '@fleetbase/fleetops-engine' }
+     * );
+     */
+    registerRenderableComponent(registryName, component, options = {}) {
+        // Handle arrays
+        if (isArray(component)) {
+            component.forEach((comp) => this.registerRenderableComponent(registryName, comp, options));
+            return;
+        }
+ 
+        // Generate unique key for the component
+        const key = component._registryKey || component.name || component.path || `component-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ 
+        // Register to RegistryService using map-based structure
+        // Structure: registries.get(registryName).components = [component1, component2, ...]
+        this.register(registryName, 'components', key, component);
+    }
+ 
+    /**
+     * Get renderable components from a registry
+     *
+     * @method getRenderableComponents
+     * @param {String} registryName Registry name
+     * @returns {Array} Array of component definitions/classes
+     */
+    getRenderableComponents(registryName) {
+        return this.getRegistry(registryName, 'components');
+    }
+ 
+    /**
+     * Create a registry (section with default list).
+     * For backward compatibility with existing code.
+     * Creates a section with a 'menu-item' list by default.
+     *
+     * @method createRegistry
+     * @param {String} sectionName Section name
+     * @param {String} type Type of registry
+     * @returns {Array} The default list array
+     */
+    createRegistry(sectionName, type = 'menu-item') {
+        return this.getOrCreateList(sectionName, type);
+    }
+ 
+    /**
+     * Create multiple registries
+     *
+     * @method createRegistries
+     * @param {Array} sectionNames Array of section names
+     */
+    createRegistries(sectionNames) {
+        if (isArray(sectionNames)) {
+            sectionNames.forEach((sectionName) => this.createRegistry(sectionName));
+        }
+    }
+ 
+    /**
+     * Create a registry section (or get existing).
+     * This is a convenience method for explicitly creating sections.
+     *
+     * @method createSection
+     * @param {String} sectionName Section name
+     * @returns {TrackedObject} The section object
+     */
+    createSection(sectionName) {
+        return this.getOrCreateSection(sectionName);
+    }
+ 
+    /**
+     * Create multiple registry sections
+     *
+     * @method createSections
+     * @param {Array} sectionNames Array of section names
+     */
+    createSections(sectionNames) {
+        if (isArray(sectionNames)) {
+            sectionNames.forEach((sectionName) => this.createSection(sectionName));
+        }
+    }
+ 
+    /**
+     * Check if a section exists
+     *
+     * @method hasSection
+     * @param {String} sectionName Section name
+     * @returns {Boolean} True if section exists
+     */
+    hasSection(sectionName) {
+        return this.registries.has(sectionName);
+    }
+ 
+    /**
+     * Check if a list exists within a section
+     *
+     * @method hasList
+     * @param {String} sectionName Section name
+     * @param {String} listName List name
+     * @returns {Boolean} True if list exists
+     */
+    hasList(sectionName, listName) {
+        const section = this.registries.get(sectionName);
+        return !!(section && section[listName]);
+    }
+ 
+    /**
+     * Clear a specific list within a section
+     *
+     * @method clearList
+     * @param {String} sectionName Section name
+     * @param {String} listName List name
+     */
+    clearList(sectionName, listName) {
+        const section = this.registries.get(sectionName);
+        if (section && section[listName]) {
+            section[listName].clear();
+        }
+    }
+ 
+    /**
+     * Clear an entire section (all lists)
+     *
+     * @method clearSection
+     * @param {String} sectionName Section name
+     */
+    clearSection(sectionName) {
+        const section = this.registries.get(sectionName);
+        if (section) {
+            Object.keys(section).forEach((listName) => {
+                if (section[listName] && typeof section[listName].clear === 'function') {
+                    section[listName].clear();
+                }
+            });
+            this.registries.delete(sectionName);
+        }
+    }
+ 
+    /**
+     * Clear all registries
+     *
+     * @method clearAll
+     */
+    clearAll() {
+        this.registries.forEach((section) => {
+            Object.keys(section).forEach((listName) => {
+                if (section[listName] && typeof section[listName].clear === 'function') {
+                    section[listName].clear();
+                }
+            });
+        });
+        this.registries.clear();
+    }
+ 
+    /**
+     * Registers a component to the root application container.
+     * This ensures the component is available to all engines and the host app.
+     * @method registerComponent
+     * @param {String} name The component name (e.g., 'my-component')
+     * @param {Class} componentClass The component class
+     * @param {Object} options Registration options (e.g., { singleton: true })
+     */
+    registerComponent(name, componentClass, options = {}) {
+        if (this.applicationInstance) {
+            this.applicationInstance.register(`component:${name}`, componentClass, options);
+        } else {
+            warn('Application instance not set on RegistryService. Cannot register component.', { id: 'registry-service.no-app-instance' });
+        }
+    }
+ 
+    /**
+     * Registers a service to the root application container.
+     * This ensures the service is available to all engines and the host app.
+     * @method registerService
+     * @param {String} name The service name (e.g., 'my-service')
+     * @param {Class} serviceClass The service class
+     * @param {Object} options Registration options (e.g., { singleton: true })
+     */
+    registerService(name, serviceClass, options = {}) {
+        if (this.applicationInstance) {
+            this.applicationInstance.register(`service:${name}`, serviceClass, options);
+        } else {
+            warn('Application instance not set on RegistryService. Cannot register service.', { id: 'registry-service.no-app-instance' });
+        }
+    }
+ 
+    /**
+     * Registers a helper to the root application container.
+     * This makes the helper available globally to all engines and the host app.
+     * Supports both direct helper functions/classes and lazy loading via TemplateHelper.
+     *
+     * @method registerHelper
+     * @param {String} helperName The helper name (e.g., 'calculate-delivery-fee')
+     * @param {Function|Class|TemplateHelper} helperClassOrTemplateHelper Helper function, class, or TemplateHelper instance
+     * @param {Object} options Registration options
+     * @param {Boolean} options.instantiate Whether to instantiate the helper (default: false for functions)
+     * @returns {Promise<void>}
+     *
+     * @example
+     * // Direct function registration
+     * await registryService.registerHelper('calculate-delivery-fee', calculateDeliveryFeeHelper);
+     *
+     * @example
+     * // Direct class registration
+     * await registryService.registerHelper('format-currency', FormatCurrencyHelper);
+     *
+     * @example
+     * // Lazy loading from engine (ensures engine is loaded first)
+     * await registryService.registerHelper(
+     *     'calculate-delivery-fee',
+     *     new TemplateHelper('@fleetbase/storefront-engine', 'helpers/calculate-delivery-fee')
+     * );
+     */
+    async registerHelper(helperName, helperClassOrTemplateHelper, options = {}) {
+        const owner = this.applicationInstance || getOwner(this);
+ 
+        if (!owner) {
+            warn('No owner available for helper registration. Cannot register helper.', {
+                id: 'registry-service.no-owner',
+            });
+            return;
+        }
+ 
+        // Check if it's a TemplateHelper instance
+        if (helperClassOrTemplateHelper instanceof TemplateHelper) {
+            const templateHelper = helperClassOrTemplateHelper;
+ 
+            if (templateHelper.isClass) {
+                // Direct class registration from TemplateHelper
+                owner.register(`helper:${helperName}`, templateHelper.class, {
+                    instantiate: options.instantiate !== undefined ? options.instantiate : true,
+                });
+            } else {
+                // Lazy loading from engine (async - ensures engine is loaded)
+                const helper = await this.#loadHelperFromEngine(templateHelper);
+                if (helper) {
+                    owner.register(`helper:${helperName}`, helper, {
+                        instantiate: options.instantiate !== undefined ? options.instantiate : true,
+                    });
+                } else {
+                    warn(`Failed to load helper from engine: ${templateHelper.engineName}/${templateHelper.path}`, {
+                        id: 'registry-service.helper-load-failed',
+                    });
+                }
+            }
+        } else {
+            // Direct function or class registration
+            // A Helper subclass is instantiated; a plain function helper is not, however it was written.
+            const instantiate = options.instantiate !== undefined ? options.instantiate : typeof helperClassOrTemplateHelper !== 'function' || isHelperClass(helperClassOrTemplateHelper);
+ 
+            owner.register(`helper:${helperName}`, helperClassOrTemplateHelper, {
+                instantiate,
+            });
+        }
+    }
+ 
+    /**
+     * Loads a helper from an engine using TemplateHelper definition.
+     * Ensures the engine is loaded before attempting to resolve the helper.
+     * @private
+     * @method #loadHelperFromEngine
+     * @param {TemplateHelper} templateHelper The TemplateHelper instance
+     * @returns {Promise<Function|Class|null>} The loaded helper or null if failed
+     */
+    async #loadHelperFromEngine(templateHelper) {
+        // No owner check here: registerHelper is the only caller and has already
+        // returned on the identical expression, so a second one could never fire.
+        try {
+            // Ensure the engine is loaded (will load if not already loaded)
+            const engineInstance = await this.extensionManager.ensureEngineLoaded(templateHelper.engineName);
+ 
+            if (!engineInstance) {
+                warn(`Engine could not be loaded: ${templateHelper.engineName}`, {
+                    id: 'registry-service.engine-not-loaded',
+                });
+                return null;
+            }
+ 
+            // Try to resolve the helper from the engine
+            const helperPath = templateHelper.path.startsWith('helper:') ? templateHelper.path : `helper:${templateHelper.path}`;
+ 
+            const helper = engineInstance.resolveRegistration(helperPath);
+ 
+            if (!helper) {
+                warn(`Helper not found in engine: ${helperPath}`, {
+                    id: 'registry-service.helper-not-found',
+                });
+                return null;
+            }
+ 
+            return helper;
+        } catch (error) {
+            warn(`Error loading helper from engine: ${error.message}`, {
+                id: 'registry-service.helper-load-error',
+            });
+            return null;
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/resource-view-service.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/resource-view-service.js.html new file mode 100644 index 0000000..1337271 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/resource-view-service.js.html @@ -0,0 +1,1177 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/resource-view-service.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe resource-view-service.js

+
+ +
+ 0% + Statements + 0/85 +
+ + +
+ 0% + Branches + 0/68 +
+ + +
+ 0% + Functions + 0/24 +
+ + +
+ 0% + Lines + 0/83 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import Service, { inject as service } from '@ember/service';
+import { warn } from '@ember/debug';
+import { isArray } from '@ember/array';
+import { getOwner } from '@ember/application';
+import mergeRegisteredItems from '../../utils/merge-registered-items';
+import isObject from '../../utils/is-object';
+import { parseRegistryName, parseRegistryPrefix, buildRegistryName, resourceRegistryNames } from '../../utils/resource-view-registry';
+ 
+/**
+ * ResourceViewService
+ *
+ * Lets extensions add columns, actions and buttons to the table and details
+ * views of any engine, and lets those views merge them in.
+ *
+ * Registry names follow `<extension>:<resource>:<surface>:<slot>`:
+ *
+ * | surface   | slot           | adds                                     | contract         |
+ * |-----------|----------------|------------------------------------------|------------------|
+ * | `table`   | `columns`      | table columns                            | `TableColumn`    |
+ * | `table`   | `row-actions`  | items in each row's dropdown             | `ResourceAction` |
+ * | `table`   | `bulk-actions` | items in the Bulk Actions dropdown       | `ResourceAction` |
+ * | `table`   | `actions`      | toolbar buttons                          | `ActionButton`   |
+ * | `details` | `actions`      | details header buttons                   | `ActionButton`   |
+ * | `details` | `menu`         | items in the details header's "…" menu   | `ResourceAction` |
+ * | `details` | `tabs`         | details tabs                             | `MenuItem`       |
+ *
+ * Items are stored in the shared universe registry: the section is the full
+ * registry name and the list is the contract type, e.g.
+ * `registryService.getRegistry('fleet-ops:driver:table:columns', 'table-column')`.
+ * Tabs are the exception: `<extension>:<resource>:details:tabs` is an alias for the
+ * legacy tab registry, `<extension>:component:<resource>:details`.
+ *
+ * @example
+ * const views = universe.getService('resource-view');
+ * views.register('fleet-ops:driver:table:row-actions', new ResourceAction({ id: 'acme-sync', label: 'Sync', fn: (driver) => … }));
+ * views.registerTableColumn('ledger', 'invoice', new TableColumn({ id: 'po', label: 'PO', valuePath: 'meta.po' }));
+ *
+ * @class ResourceViewService
+ * @extends Service
+ */
+export default class ResourceViewService extends Service {
+    @service('universe/registry-service') registryService;
+    @service('universe/menu-service') menuService;
+ 
+    /**
+     * Views whose query params were already read by a controller. A filter
+     * column registered for one of these afterwards cannot get its param.
+     */
+    #resolvedQueryParams = new Set();
+ 
+    // ========================================================================
+    // Naming
+    // ========================================================================
+ 
+    /**
+     * Every registry name for one resource's table and details views.
+     *
+     * @param {String} extension
+     * @param {String} resource
+     * @returns {Array<String>}
+     */
+    resourceRegistryNames(extension, resource) {
+        return resourceRegistryNames(extension, resource);
+    }
+ 
+    /**
+     * Declare the registries for resources an engine owns, so they exist (and
+     * show up in the registry) before anything registers into them.
+     *
+     * @param {String} extension The owning engine's mount segment
+     * @param {Array<String>} resources
+     * @returns {Array<String>} The declared names
+     */
+    declare(extension, resources = []) {
+        const names = resources.flatMap((resource) => resourceRegistryNames(extension, resource));
+        names.forEach((name) => {
+            const { section, list } = parseRegistryName(name);
+            this.registryService.getOrCreateList(section, list);
+        });
+        return names;
+    }
+ 
+    // ========================================================================
+    // Registering
+    // ========================================================================
+ 
+    /**
+     * Register one or more items into a view slot.
+     *
+     * Rejects, with a warning, a name that does not follow the convention, an
+     * item without an `id`, and a contract that does not belong in the slot.
+     *
+     * @param {String} registryName e.g. `fleet-ops:driver:table:columns`
+     * @param {Object|Array} items Contracts or plain objects
+     * @returns {Boolean} Whether every item was registered
+     */
+    register(registryName, items) {
+        const parsed = parseRegistryName(registryName);
+        if (!parsed) {
+            warn(`[resource-view] "${registryName}" is not a valid registry name. Use <extension>:<resource>:<table|details>:<slot>.`, false, {
+                id: 'resource-view.invalid-name',
+            });
+            return false;
+        }
+ 
+        return (isArray(items) ? items : [items]).map((item) => this.#registerItem(parsed, item)).every(Boolean);
+    }
+ 
+    registerTableColumn(extension, resource, column) {
+        return this.register(buildRegistryName(extension, resource, 'table', 'columns'), column);
+    }
+ 
+    registerRowAction(extension, resource, action) {
+        return this.register(buildRegistryName(extension, resource, 'table', 'row-actions'), action);
+    }
+ 
+    registerBulkAction(extension, resource, action) {
+        return this.register(buildRegistryName(extension, resource, 'table', 'bulk-actions'), action);
+    }
+ 
+    registerTableAction(extension, resource, button) {
+        return this.register(buildRegistryName(extension, resource, 'table', 'actions'), button);
+    }
+ 
+    registerDetailsAction(extension, resource, button) {
+        return this.register(buildRegistryName(extension, resource, 'details', 'actions'), button);
+    }
+ 
+    registerDetailsMenuItem(extension, resource, action) {
+        return this.register(buildRegistryName(extension, resource, 'details', 'menu'), action);
+    }
+ 
+    /**
+     * Register a details tab, through `<extension>:<resource>:details:tabs`, the
+     * alias for `<extension>:component:<resource>:details`.
+     *
+     * @returns {Boolean}
+     */
+    registerDetailsTab(extension, resource, menuItem) {
+        return this.register(buildRegistryName(extension, resource, 'details', 'tabs'), menuItem);
+    }
+ 
+    /**
+     * Remove a registered item.
+     *
+     * @param {String} registryName
+     * @param {String} id
+     * @returns {Boolean} Whether an item was removed
+     */
+    unregister(registryName, id) {
+        const parsed = parseRegistryName(registryName);
+        if (!parsed) {
+            return false;
+        }
+ 
+        const item = this.registryService.lookup(parsed.section, parsed.list, id);
+        if (!item) {
+            return false;
+        }
+ 
+        this.registryService.getRegistry(parsed.section, parsed.list).removeObject(item);
+        return true;
+    }
+ 
+    // ========================================================================
+    // Reading
+    // ========================================================================
+ 
+    /**
+     * The items registered into a slot, in registration order.
+     *
+     * @param {String} registryName
+     * @returns {Array}
+     */
+    get(registryName) {
+        const parsed = parseRegistryName(registryName);
+        return parsed ? this.registryService.getRegistry(parsed.section, parsed.list) : [];
+    }
+ 
+    /**
+     * Merge a slot's registered items into a view's built-in items. See
+     * `utils/merge-registered-items` for placement and handler binding.
+     *
+     * @param {String} registryName
+     * @param {Array} baseItems
+     * @param {Object} context Extra view context (`resource`, `controller`, `table`, …)
+     * @returns {Array}
+     */
+    merge(registryName, baseItems = [], context = {}) {
+        const parsed = parseRegistryName(registryName);
+        if (!parsed) {
+            return toPlainArray(baseItems);
+        }
+ 
+        const registered = this.get(registryName);
+        if (!registered.length) {
+            return toPlainArray(baseItems);
+        }
+ 
+        return mergeRegisteredItems(baseItems, registered, {
+            slot: parsed.slot,
+            context: this.#buildContext(parsed, context),
+            canAccess: (permission) => this.#can(permission),
+        });
+    }
+ 
+    /**
+     * Merge one slot of a surface, addressed by the `@registry` prefix a layout
+     * component receives (`fleet-ops:driver:table`) plus the slot name.
+     *
+     * @param {String} prefix
+     * @param {String} slot
+     * @param {Array} baseItems
+     * @param {Object} context
+     * @returns {Array}
+     */
+    mergeSlot(prefix, slot, baseItems = [], context = {}) {
+        const parsed = parseRegistryPrefix(prefix);
+        if (!parsed) {
+            return toPlainArray(baseItems);
+        }
+ 
+        return this.merge(`${prefix}:${slot}`, baseItems, context);
+    }
+ 
+    /**
+     * A table's row-actions column with registered row actions merged into its
+     * `actions`. Returns the column list unchanged when nothing is registered.
+     *
+     * @param {String} prefix A table prefix, e.g. `fleet-ops:driver:table`
+     * @param {Array} columns
+     * @param {Object} context
+     * @returns {Array}
+     */
+    mergeRowActions(prefix, columns = [], context = {}) {
+        const registryName = `${prefix}:row-actions`;
+        if (!parseRegistryName(registryName) || !this.get(registryName).length) {
+            return toPlainArray(columns);
+        }
+ 
+        return toPlainArray(columns).map((column) => {
+            if (column?.cellComponent !== 'table/cell/dropdown') {
+                return column;
+            }
+ 
+            return { ...column, actions: this.merge(registryName, column.actions ?? [], context) };
+        });
+    }
+ 
+    // ========================================================================
+    // Filters
+    // ========================================================================
+ 
+    /**
+     * The query params an index controller must declare: its own, plus the
+     * `filterParam` of every filterable registered column.
+     *
+     * Call it from the controller's `queryParams` class field. Filter columns
+     * must therefore be registered in `setupExtension`, before the controller
+     * is first created.
+     *
+     * @param {String} extension
+     * @param {String} resource
+     * @param {Array} baseQueryParams
+     * @returns {Array}
+     */
+    queryParamsFor(extension, resource, baseQueryParams = []) {
+        const registryName = buildRegistryName(extension, resource, 'table', 'columns');
+        this.#resolvedQueryParams.add(registryName);
+ 
+        const queryParams = [...baseQueryParams];
+        for (const param of this.get(registryName).map(filterParamOf).filter(Boolean)) {
+            if (!queryParams.includes(param)) {
+                queryParams.push(param);
+            }
+        }
+ 
+        return queryParams;
+    }
+ 
+    // ========================================================================
+    // Internals
+    // ========================================================================
+ 
+    #registerItem(parsed, item) {
+        // Tabs go to the legacy tab registry, through the menu service, which keys and
+        // wraps menu items as every other tab registration expects.
+        if (parsed.slot === 'tabs') {
+            if (!isObject(item)) {
+                warn(`[resource-view] Tabs registered into "${parsed.name}" must be MenuItems or menu item objects.`, false, { id: 'resource-view.invalid-tab' });
+                return false;
+            }
+ 
+            this.menuService.registerMenuItem(parsed.section, item);
+            return true;
+        }
+ 
+        const value = normalizeItem(item);
+ 
+        if (!value || !value.id) {
+            warn(`[resource-view] Items registered into "${parsed.name}" need an id.`, false, { id: 'resource-view.missing-id' });
+            return false;
+        }
+ 
+        if (value._contractType && value._contractType !== parsed.list) {
+            warn(`[resource-view] "${value.id}" is a ${value._contractType}, but "${parsed.name}" holds ${parsed.list} items.`, false, {
+                id: 'resource-view.wrong-contract',
+            });
+            return false;
+        }
+ 
+        if (parsed.slot === 'columns' && filterParamOf(value) && this.#resolvedQueryParams.has(parsed.name)) {
+            warn(
+                `[resource-view] The filter "${value.filterParam}" on "${value.id}" was registered after its table was set up, so it is not a query param. Register filter columns in setupExtension.`,
+                false,
+                { id: 'resource-view.late-filter' }
+            );
+        }
+ 
+        this.registryService.register(parsed.section, parsed.list, value.id, { ...value, _contractType: parsed.list });
+        return true;
+    }
+ 
+    #buildContext(parsed, context) {
+        const base = {
+            registry: parsed.name,
+            extension: parsed.extension,
+            surface: parsed.surface,
+            resourceName: parsed.resource,
+            slot: parsed.slot,
+            owner: getOwner(this),
+        };
+ 
+        // Copy descriptors rather than spreading: a view may pass lazy getters
+        // (its table is set up after first render), and a spread would read
+        // them now, during the render that merges.
+        return Object.defineProperties(base, Object.getOwnPropertyDescriptors(context));
+    }
+ 
+    #can(permission) {
+        const abilities = getOwner(this)?.lookup('service:abilities');
+        return !abilities || typeof abilities.can !== 'function' || abilities.can(permission);
+    }
+}
+ 
+function normalizeItem(item) {
+    if (item && typeof item.toObject === 'function') {
+        return item.toObject();
+    }
+ 
+    return isObject(item) ? { ...item } : null;
+}
+ 
+function filterParamOf(column) {
+    return column?.filterable && typeof column.filterParam === 'string' ? column.filterParam : null;
+}
+ 
+function toPlainArray(items) {
+    if (!items) {
+        return [];
+    }
+ 
+    return typeof items.toArray === 'function' ? items.toArray() : Array.from(items);
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/widget-service.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/widget-service.js.html new file mode 100644 index 0000000..12a8471 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/widget-service.js.html @@ -0,0 +1,1201 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe/widget-service.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/universe widget-service.js

+
+ +
+ 0% + Statements + 0/64 +
+ + +
+ 0% + Branches + 0/48 +
+ + +
+ 0% + Functions + 0/24 +
+ + +
+ 0% + Lines + 0/62 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import Service from '@ember/service';
+import { inject as service } from '@ember/service';
+import { warn } from '@ember/debug';
+import { isArray } from '@ember/array';
+import { tracked } from '@glimmer/tracking';
+import Widget from '../../contracts/widget';
+import isObject from '../../utils/is-object';
+ 
+/**
+ * WidgetManagerService
+ *
+ * Manages dashboard widgets and widget registrations.
+ *
+ * Widgets are registered per-dashboard:
+ * - registerWidgets(dashboardName, widgets) - Makes widgets available for selection on a dashboard
+ * - registerDefaultWidgets(dashboardName, widgets) - Auto-loads specific widgets on a dashboard
+ *
+ * Registry Structure:
+ * - Dashboards: 'dashboards' section, 'dashboard' list
+ * - Widgets: 'dashboard:widgets' section, 'widget' list
+ * - Default Widgets: 'dashboard:widgets' section, 'default-widget' list
+ *
+ * @class WidgetService
+ * @extends Service
+ */
+export default class WidgetService extends Service {
+    @service('universe/registry-service') registry;
+ 
+    /**
+     * Reference to the root Ember Application Instance.
+     * Used for registering components/services to the application container
+     * for cross-engine sharing.
+     */
+    @tracked applicationInstance = null;
+ 
+    /**
+     * Set the application instance (for consistency with other services)
+     *
+     * @method setApplicationInstance
+     * @param {Application} application The root application instance
+     */
+    setApplicationInstance(application) {
+        this.applicationInstance = application;
+    }
+ 
+    /**
+     * Normalize a widget input to a plain object
+     *
+     * @private
+     * @method #normalizeWidget
+     * @param {Widget|Object} input Widget instance or object
+     * @returns {Object} Normalized widget object
+     */
+    #normalizeWidget(input) {
+        if (input instanceof Widget) {
+            return input.toObject();
+        }
+ 
+        // Handle plain objects - ensure id property exists
+        if (isObject(input)) {
+            // Support both id and widgetId for backward compatibility
+            const id = input.id || input.widgetId;
+ 
+            if (!id) {
+                warn('[WidgetService] Widget definition is missing id or widgetId', { id: 'widget-service.missing-id' });
+            }
+ 
+            return {
+                ...input,
+                id, // Ensure id property is set
+            };
+        }
+ 
+        return input;
+    }
+ 
+    /**
+     * Register a dashboard
+     *
+     * @method registerDashboard
+     * @param {String} name Dashboard name/ID
+     * @param {Object} options Dashboard options
+     */
+    registerDashboard(name, options = {}) {
+        const dashboard = {
+            name,
+            ...options,
+        };
+ 
+        // Register to 'dashboards' section, 'dashboard' list
+        this.registry.register('dashboards', 'dashboard', name, dashboard);
+    }
+ 
+    /**
+     * Register a dashboard render slot.
+     *
+     * Slots describe where dashboards render (for example `console.home`),
+     * while dashboard names continue to describe widget namespaces.
+     *
+     * @method registerDashboardSlot
+     * @param {String} slotId Slot identifier
+     * @param {Object} options Slot options
+     */
+    registerDashboardSlot(slotId, options = {}) {
+        const slot = {
+            id: slotId,
+            ...options,
+        };
+ 
+        this.registry.register('dashboard:slots', 'slot', slotId, slot);
+    }
+ 
+    /**
+     * Register a system dashboard that should be available in a render slot.
+     *
+     * @method registerDashboardForSlot
+     * @param {String} slotId Slot identifier
+     * @param {String} dashboardName Dashboard widget namespace
+     * @param {Object} options Dashboard slot options
+     */
+    registerDashboardForSlot(slotId, dashboardName, options = {}) {
+        this.registerDashboardSlot(slotId);
+        this.registerDashboard(dashboardName, options);
+ 
+        const dashboard = {
+            id: dashboardName,
+            dashboardId: dashboardName,
+            name: options.name ?? options.defaultDashboardName ?? dashboardName,
+            extension: options.extension ?? 'core',
+            priority: options.priority ?? 0,
+            ...options,
+        };
+ 
+        this.registry.register('dashboard:slots', 'dashboard', `${slotId}#${dashboardName}`, {
+            slotId,
+            ...dashboard,
+        });
+    }
+ 
+    /**
+     * Set the system dashboard that should load first for a render slot.
+     *
+     * @method setDefaultDashboardForSlot
+     * @param {String} slotId Slot identifier
+     * @param {String} dashboardName Dashboard widget namespace
+     */
+    setDefaultDashboardForSlot(slotId, dashboardName) {
+        this.registerDashboardSlot(slotId);
+        this.registry.register('dashboard:slots', 'default', slotId, {
+            slotId,
+            dashboardId: dashboardName,
+        });
+    }
+ 
+    /**
+     * Convenience alias for setting the console home dashboard.
+     *
+     * @method setConsoleDashboard
+     * @param {String} dashboardName Dashboard widget namespace
+     */
+    setConsoleDashboard(dashboardName) {
+        this.setDefaultDashboardForSlot('console.home', dashboardName);
+    }
+ 
+    /**
+     * Get system dashboards registered for a slot.
+     *
+     * @method getDashboardsForSlot
+     * @param {String} slotId Slot identifier
+     * @returns {Array} Slot dashboard registrations
+     */
+    getDashboardsForSlot(slotId) {
+        if (!slotId) {
+            return [];
+        }
+ 
+        const prefix = `${slotId}#`;
+        return [...this.registry.getRegistry('dashboard:slots', 'dashboard')]
+            .filter((dashboard) => dashboard?._registryKey?.startsWith(prefix))
+            .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
+    }
+ 
+    /**
+     * Get the dashboard namespace that should load first for a slot.
+     *
+     * @method getDefaultDashboardForSlot
+     * @param {String} slotId Slot identifier
+     * @returns {String|null} Dashboard namespace
+     */
+    getDefaultDashboardForSlot(slotId) {
+        return this.registry.lookup('dashboard:slots', 'default', slotId)?.dashboardId ?? null;
+    }
+ 
+    /**
+     * Register widgets to a specific dashboard
+     * Makes these widgets available for selection on the dashboard
+     * If a widget has `default: true`, it's also registered as a default widget
+     *
+     * @method registerWidgets
+     * @param {String} dashboardName Dashboard name/ID
+     * @param {Array<Widget>} widgets Array of widget instances or objects
+     */
+    registerWidgets(dashboardName, widgets) {
+        if (!isArray(widgets)) {
+            widgets = [widgets];
+        }
+ 
+        widgets.forEach((widget) => {
+            const normalized = this.#normalizeWidget(widget);
+ 
+            // Register widget to 'dashboard:widgets' section, 'widget' list
+            // Key format: dashboardName#widgetId
+            this.registry.register('dashboard:widgets', 'widget', `${dashboardName}#${normalized.id}`, normalized);
+ 
+            // If marked as default, also register to default widget list
+            if (normalized.default === true) {
+                this.registry.register('dashboard:widgets', 'default-widget', `${dashboardName}#${normalized.id}`, normalized);
+            }
+        });
+    }
+ 
+    /**
+     * Register default widgets for a specific dashboard
+     * These widgets are automatically loaded on the dashboard
+     *
+     * @method registerDefaultWidgets
+     * @param {String} dashboardName Dashboard name/ID
+     * @param {Array<Widget>} widgets Array of widget instances or objects
+     */
+    registerDefaultWidgets(dashboardName, widgets) {
+        if (!isArray(widgets)) {
+            widgets = [widgets];
+        }
+ 
+        widgets.forEach((widget) => {
+            const normalized = this.#normalizeWidget(widget);
+ 
+            // Register to 'dashboard:widgets' section, 'default-widget' list
+            // Key format: dashboardName#widgetId
+            this.registry.register('dashboard:widgets', 'default-widget', `${dashboardName}#${normalized.id}`, normalized);
+        });
+    }
+ 
+    /**
+     * Get widgets for a specific dashboard
+     * Returns all widgets available for selection on that dashboard
+     *
+     * @method getWidgets
+     * @param {String} dashboardName Dashboard name/ID
+     * @returns {Array} Widgets available for the dashboard
+     */
+    getWidgets(dashboardName) {
+        if (!dashboardName) {
+            return [];
+        }
+ 
+        // Get all widgets from 'dashboard:widgets' section, 'widget' list
+        const registry = this.registry.getRegistry('dashboard:widgets', 'widget');
+ 
+        // Filter widgets by registration key prefix
+        const prefix = `${dashboardName}#`;
+ 
+        return registry.filter((widget) => {
+            if (!widget || typeof widget !== 'object') return false;
+ 
+            // Match widgets registered for this dashboard
+            return widget._registryKey && widget._registryKey.startsWith(prefix);
+        });
+    }
+ 
+    /**
+     * Get default widgets for a specific dashboard
+     * Returns widgets that should be auto-loaded
+     *
+     * @method getDefaultWidgets
+     * @param {String} dashboardName Dashboard name/ID
+     * @returns {Array} Default widgets for the dashboard
+     */
+    getDefaultWidgets(dashboardName) {
+        if (!dashboardName) {
+            return [];
+        }
+ 
+        // Get all default widgets from 'dashboard:widgets' section, 'default-widget' list
+        const registry = this.registry.getRegistry('dashboard:widgets', 'default-widget');
+ 
+        // Filter widgets by registration key prefix
+        const prefix = `${dashboardName}#`;
+ 
+        return registry.filter((widget) => {
+            if (!widget || typeof widget !== 'object') return false;
+ 
+            // Match default widgets registered for this dashboard
+            return widget._registryKey && widget._registryKey.startsWith(prefix);
+        });
+    }
+ 
+    /**
+     * Get a specific widget by ID from a dashboard
+     *
+     * @method getWidget
+     * @param {String} dashboardName Dashboard name/ID
+     * @param {String} widgetId Widget ID
+     * @returns {Object|null} Widget or null
+     */
+    getWidget(dashboardName, widgetId) {
+        return this.registry.lookup('dashboard:widgets', 'widget', `${dashboardName}#${widgetId}`);
+    }
+ 
+    /**
+     * Get all dashboards
+     *
+     * @method getDashboards
+     * @returns {Array} All dashboards
+     */
+    getDashboards() {
+        return this.registry.getRegistry('dashboards', 'dashboard');
+    }
+ 
+    /**
+     * Get a specific dashboard
+     *
+     * @method getDashboard
+     * @param {String} name Dashboard name
+     * @returns {Object|null} Dashboard or null
+     */
+    getDashboard(name) {
+        return this.registry.lookup('dashboards', 'dashboard', name);
+    }
+ 
+    /**
+     * Get registry for a specific dashboard
+     * Used by dashboard models to get their widget registry
+     *
+     * @method getRegistry
+     * @param {String} dashboardId Dashboard ID
+     * @returns {Array} Widget registry for the dashboard
+     */
+    getRegistry(dashboardId) {
+        return this.getWidgets(dashboardId);
+    }
+ 
+    // ============================================================================
+    // DEPRECATED METHODS (for backward compatibility)
+    // ============================================================================
+ 
+    /**
+     * Register default dashboard widgets
+     * DEPRECATED: Use registerDefaultWidgets(dashboardName, widgets) instead
+     *
+     * @method registerDefaultDashboardWidgets
+     * @param {Array<Widget>} widgets Array of widget instances or objects
+     * @deprecated Use registerDefaultWidgets('dashboard', widgets) instead
+     */
+    registerDefaultDashboardWidgets(widgets) {
+        warn('[WidgetService] registerDefaultDashboardWidgets is deprecated. Use registerDefaultWidgets(dashboardName, widgets) instead.', { id: 'widget-service.deprecated-method' });
+        this.registerDefaultWidgets('dashboard', widgets);
+    }
+ 
+    /**
+     * Register dashboard widgets
+     * DEPRECATED: Use registerWidgets(dashboardName, widgets) instead
+     *
+     * @method registerDashboardWidgets
+     * @param {Array<Widget>} widgets Array of widget instances or objects
+     * @deprecated Use registerWidgets('dashboard', widgets) instead
+     */
+    registerDashboardWidgets(widgets) {
+        warn('[WidgetService] registerDashboardWidgets is deprecated. Use registerWidgets(dashboardName, widgets) instead.', { id: 'widget-service.deprecated-method' });
+        this.registerWidgets('dashboard', widgets);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/url-search-params.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/url-search-params.js.html new file mode 100644 index 0000000..f2216e3 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/url-search-params.js.html @@ -0,0 +1,919 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services/url-search-params.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/services url-search-params.js

+
+ +
+ 0% + Statements + 0/52 +
+ + +
+ 0% + Branches + 0/9 +
+ + +
+ 0% + Functions + 0/20 +
+ + +
+ 0% + Lines + 0/52 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import Service from '@ember/service';
+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 working set of query parameters.
+     *
+     * Rebuilt from `window.location.search` whenever the browser's search string
+     * differs from the one these params were built from, so a navigation is
+     * picked up; otherwise the same instance is handed back, so `setParam`,
+     * `addParam` and `removeParam` actually persist until `updateUrl` publishes
+     * them.
+     *
+     * @type {URLSearchParams}
+     * @private
+     */
+    #params = null;
+    #builtFrom = null;
+ 
+    get urlParams() {
+        return this.#currentParams();
+    }
+ 
+    /**
+     * Rebuild-if-stale, kept out of the getter body so a property read is not
+     * itself an assignment.
+     *
+     * @private
+     * @returns {URLSearchParams}
+     */
+    #currentParams() {
+        const search = window.location.search;
+ 
+        if (this.#params === null || this.#builtFrom !== search) {
+            this.#params = new URLSearchParams(search);
+            this.#builtFrom = search;
+        }
+ 
+        return this.#params;
+    }
+ 
+    set urlParams(value) {
+        this.#params = value;
+        this.#builtFrom = window.location.search;
+    }
+ 
+    /**
+     * Retrieves the value of a specific query parameter.
+     *
+     * 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) {
+        let value = this.urlParams.get(key);
+ 
+        if (hasJsonStructure(value)) {
+            value = JSON.parse(value);
+        }
+ 
+        return value;
+    }
+ 
+    /**
+     * 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 name of the query parameter to retrieve.
+     * @returns {*} The value of the query parameter.
+     */
+    get(key) {
+        return this.getParam(key);
+    }
+ 
+    /**
+     * Sets or updates a query parameter with multiple values.
+     *
+     * @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) {
+        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);
+    }
+ 
+    /**
+     * Removes a specific query parameter from the URL search parameters.
+     *
+     * @param {string} key - The name of the query parameter to remove.
+     * @returns {this} Returns the service instance for chaining.
+     */
+    remove(key) {
+        this.urlParams.delete(key);
+ 
+        return this;
+    }
+ 
+    /**
+     * Retrieves all query parameters as an object.
+     *
+     * 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() {
+        const all = {};
+ 
+        for (let key of this.urlParams.keys()) {
+            all[key] = this.getParam(key);
+        }
+ 
+        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);
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/array.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/array.js.html new file mode 100644 index 0000000..b8cd5f9 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/array.js.html @@ -0,0 +1,172 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/array.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms array.js

+
+ +
+ 30% + Statements + 3/10 +
+ + +
+ 25% + Branches + 2/8 +
+ + +
+ 50% + Functions + 1/2 +
+ + +
+ 30% + Lines + 3/10 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +  +  +  +3x +3x +  +  +  +  +  + 
import Transform from '@ember-data/serializer/transform';
+import isIterable from '../utils/is-iterable';
+import { isArray } from '@ember/array';
+ 
+export default class ArrayTransform extends Transform {
+    deserialize(serialized) {
+        if (isArray(serialized)) {
+            return serialized;
+        }
+ 
+        if (!isIterable(serialized)) {
+            return [];
+        }
+ 
+        return Array.from(serialized);
+    }
+ 
+    serialize(deserialized) {
+        Iif (isArray(deserialized)) {
+            return deserialized;
+        }
+ 
+        Eif (!isIterable(deserialized)) {
+            return [];
+        }
+ 
+        return Array.from(deserialized);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/index.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/index.html new file mode 100644 index 0000000..ec08e4b --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms + + + + + + + + + +
+
+

All files ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms

+
+ +
+ 42.85% + Statements + 6/14 +
+ + +
+ 25% + Branches + 2/8 +
+ + +
+ 66.66% + Functions + 4/6 +
+ + +
+ 42.85% + Lines + 6/14 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
array.js +
+
30%3/1025%2/850%1/230%3/10
object.js +
+
50%1/2100%0/050%1/250%1/2
raw.js +
+
100%2/2100%0/0100%2/2100%2/2
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/object.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/object.js.html new file mode 100644 index 0000000..e1a30a5 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/object.js.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/object.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms object.js

+
+ +
+ 50% + Statements + 1/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 50% + Functions + 1/2 +
+ + +
+ 50% + Lines + 1/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +  +  +  +  +  +  +39x +  +  + 
import Transform from '@ember-data/serializer/transform';
+ 
+export default class ObjectTransform extends Transform {
+    deserialize(serialized) {
+        return Object.assign({}, serialized);
+    }
+ 
+    serialize(deserialized) {
+        return Object.assign({}, deserialized);
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/raw.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/raw.js.html new file mode 100644 index 0000000..80489b9 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/raw.js.html @@ -0,0 +1,118 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms/raw.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/transforms raw.js

+
+ +
+ 100% + Statements + 2/2 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 100% + Lines + 2/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12  +  +  +  +4x +  +  +  +148x +  +  + 
import Transform from '@ember-data/serializer/transform';
+ 
+export default class RawTransform extends Transform {
+    deserialize(serialized) {
+        return serialized;
+    }
+ 
+    serialize(deserialized) {
+        return deserialized;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/corslite.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/corslite.js.html new file mode 100644 index 0000000..54039b6 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/corslite.js.html @@ -0,0 +1,385 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/corslite.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils corslite.js

+
+ +
+ 0% + Statements + 0/37 +
+ + +
+ 0% + Branches + 0/23 +
+ + +
+ 0% + Functions + 0/13 +
+ + +
+ 0% + Lines + 0/37 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { later } from '@ember/runloop';
+ 
+function corslite(url, callback, cors) {
+    var sent = false;
+ 
+    if (typeof window.XMLHttpRequest === 'undefined') {
+        return callback(Error('Browser not supported'));
+    }
+ 
+    if (typeof cors === 'undefined') {
+        var m = url.match(/^\s*https?:\/\/[^\/]*/);
+        /* istanbul ignore next -- the test server always runs on an explicit port, so the portless arm of the ternary is unreachable here */
+        cors = m && m[0] !== location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '');
+    }
+ 
+    var x = new window.XMLHttpRequest();
+ 
+    function isSuccessful(status) {
+        return (status >= 200 && status < 300) || status === 304;
+    }
+ 
+    if (cors && !('withCredentials' in x)) {
+        // IE8-9
+        x = new window.XDomainRequest();
+ 
+        // Ensure callback is never called synchronously, i.e., before
+        // x.send() returns (this has been observed in the wild).
+        // See https://github.com/mapbox/mapbox.js/issues/472
+        var original = callback;
+        callback = function () {
+            if (sent) {
+                original.apply(this, arguments);
+            } else {
+                later(
+                    this,
+                    function () {
+                        original.apply(this, arguments);
+                    },
+                    0
+                );
+            }
+        };
+    }
+ 
+    function loaded() {
+        if (
+            // XDomainRequest
+            x.status === undefined ||
+            // modern browsers
+            isSuccessful(x.status)
+        )
+            callback.call(x, null, x);
+        else callback.call(x, x, null);
+    }
+ 
+    // Both `onreadystatechange` and `onload` can fire. `onreadystatechange`
+    // has [been supported for longer](http://stackoverflow.com/a/9181508/229001).
+    if ('onload' in x) {
+        x.onload = loaded;
+    } else {
+        x.onreadystatechange = function readystate() {
+            if (x.readyState === 4) {
+                loaded();
+            }
+        };
+    }
+ 
+    // Call the callback with the XMLHttpRequest object as an error and prevent
+    // it from ever being called again by reassigning it to `noop`
+    x.onerror = function error(evt) {
+        // XDomainRequest provides no evt parameter
+        callback.call(this, evt || true, null);
+        callback = function () {};
+    };
+ 
+    // IE9 must have onprogress be set to a unique function.
+    x.onprogress = function () {};
+ 
+    x.ontimeout = function (evt) {
+        callback.call(this, evt, null);
+        callback = function () {};
+    };
+ 
+    x.onabort = function (evt) {
+        callback.call(this, evt, null);
+        callback = function () {};
+    };
+ 
+    // GET is the only supported HTTP Verb by XDomainRequest and is the
+    // only one supported here.
+    x.open('GET', url, true);
+ 
+    // Send the request. Sending data is not supported.
+    x.send(null);
+    sent = true;
+ 
+    return x;
+}
+ 
+export default corslite;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/download.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/download.js.html new file mode 100644 index 0000000..3fe542b --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/download.js.html @@ -0,0 +1,151 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/download.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils download.js

+
+ +
+ 0% + Statements + 0/89 +
+ + +
+ 0% + Branches + 0/55 +
+ + +
+ 0% + Functions + 0/10 +
+ + +
+ 0% + Lines + 0/86 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +230 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0
Cannot read properties of undefined (reading 'start')
+TypeError: Cannot read properties of undefined (reading 'start')
+    at /Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/istanbul-reports@3.2.0/node_modules/istanbul-reports/lib/html/annotator.js:194:41
+    at Array.forEach ()
+    at annotateBranches (/Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/istanbul-reports@3.2.0/node_modules/istanbul-reports/lib/html/annotator.js:128:33)
+    at annotateSourceCode (/Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/istanbul-reports@3.2.0/node_modules/istanbul-reports/lib/html/annotator.js:269:9)
+    at HtmlReport.onDetail (/Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/istanbul-reports@3.2.0/node_modules/istanbul-reports/lib/html/index.js:414:33)
+    at LcovReport. [as onDetail] (/Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/istanbul-reports@3.2.0/node_modules/istanbul-reports/lib/lcov/index.js:28:23)
+    at Visitor.value (/Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/istanbul-lib-report@3.0.1/node_modules/istanbul-lib-report/lib/tree.js:38:38)
+    at ReportNode.visit (/Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/istanbul-lib-report@3.0.1/node_modules/istanbul-lib-report/lib/tree.js:88:21)
+    at /Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/istanbul-lib-report@3.0.1/node_modules/istanbul-lib-report/lib/tree.js:92:19
+    at Array.forEach ()
+    at ReportNode.visit (/Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/istanbul-lib-report@3.0.1/node_modules/istanbul-lib-report/lib/tree.js:91:28)
+    at /Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/istanbul-lib-report@3.0.1/node_modules/istanbul-lib-report/lib/tree.js:92:19
+    at Array.forEach ()
+    at ReportNode.visit (/Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/istanbul-lib-report@3.0.1/node_modules/istanbul-lib-report/lib/tree.js:91:28)
+    at ReportTree.visit (/Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/istanbul-lib-report@3.0.1/node_modules/istanbul-lib-report/lib/tree.js:127:24)
+    at LcovReport.execute (/Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/istanbul-lib-report@3.0.1/node_modules/istanbul-lib-report/lib/report-base.js:12:44)
+    at /Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/ember-cli-code-coverage@3.1.0/node_modules/ember-cli-code-coverage/lib/attach-middleware.js:226:12
+    at Array.forEach ()
+    at reportCoverage (/Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/ember-cli-code-coverage@3.1.0/node_modules/ember-cli-code-coverage/lib/attach-middleware.js:222:13)
+    at coverageHandler (/Users/ron/Development/fleetbase/oss/fleetbase-dev/console/node_modules/.pnpm/ember-cli-code-coverage@3.1.0/node_modules/ember-cli-code-coverage/lib/attach-middleware.js:232:3)
+    at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-mime-type.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-mime-type.js.html new file mode 100644 index 0000000..ff4c83b --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-mime-type.js.html @@ -0,0 +1,154 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-mime-type.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils get-mime-type.js

+
+ +
+ 33.33% + Statements + 1/3 +
+ + +
+ 0% + Branches + 0/2 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 33.33% + Lines + 1/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +241x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
const MIME_TYPES = {
+    pdf: 'application/pdf',
+    zip: 'application/zip',
+    doc: 'application/msword',
+    xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+    docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+    png: 'image/png',
+    jpeg: 'image/jpeg',
+    jpg: 'image/jpeg',
+    csv: 'text/csv',
+};
+ 
+/**
+ * The mime type for a file name, from its extension, or null when it is not one we know.
+ *
+ * @param {String} fileName
+ * @returns {String|null}
+ */
+export default function getMimeType(fileName) {
+    const extension = String(fileName).split('.').pop().toLowerCase();
+ 
+    return MIME_TYPES[extension] ?? null;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-user-options.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-user-options.js.html new file mode 100644 index 0000000..925eab6 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-user-options.js.html @@ -0,0 +1,148 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-user-options.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils get-user-options.js

+
+ +
+ 80% + Statements + 4/5 +
+ + +
+ 50% + Branches + 1/2 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 80% + Lines + 4/5 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22  +  +  +  +  +  +  +  +  +  +86x +  +86x +86x +86x +  +  +  +  +  +  + 
/**
+ * Retrieves user options from local storage.
+ *
+ * Attempts to read the user options stored in local storage as a JSON string under the key
+ * USER_OPTIONS_STORAGE_KEY. If the JSON parsing fails, an empty object is returned.
+ *
+ * @returns {Object} The user options as an object, or an empty object if no valid user options were found.
+ * @throws Will not throw an error, even if JSON parsing fails. Errors are silently caught and ignored.
+ */
+export default function getUserOptions() {
+    const USER_OPTIONS_STORAGE_KEY = '@fleetbase/storage:user-options';
+ 
+    try {
+        const storedValue = window.localStorage.getItem(USER_OPTIONS_STORAGE_KEY);
+        return storedValue ? JSON.parse(storedValue) : {};
+    } catch (error) {
+        // For debugging purposes, you can log the error (optional)
+        // console.error('Failed to parse user options from local storage:', error);
+        return {};
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-with-default.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-with-default.js.html new file mode 100644 index 0000000..7a9e391 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-with-default.js.html @@ -0,0 +1,103 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/get-with-default.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils get-with-default.js

+
+ +
+ 0% + Statements + 0/2 +
+ + +
+ 0% + Branches + 0/2 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7  +  +  +  +  +  + 
import { get } from '@ember/object';
+ 
+export default function getWithDefault(obj, key, defaultValue) {
+    let value = get(obj, key);
+    return value === undefined ? defaultValue : value;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/has-json-structure.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/has-json-structure.js.html new file mode 100644 index 0000000..27af56e --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/has-json-structure.js.html @@ -0,0 +1,115 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/has-json-structure.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils has-json-structure.js

+
+ +
+ 0% + Statements + 0/7 +
+ + +
+ 0% + Branches + 0/4 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/6 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11  +  +  +  +  +  +  +  +  +  + 
export default function hasJsonStructure(str) {
+    if (typeof str !== 'string') return false;
+    try {
+        const result = JSON.parse(str);
+        const type = Object.prototype.toString.call(result);
+        return type === '[object Object]' || type === '[object Array]';
+    } catch (err) {
+        return false;
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/index.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/index.html new file mode 100644 index 0000000..4afb4c5 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/index.html @@ -0,0 +1,356 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils + + + + + + + + + +
+
+

All files ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils

+
+ +
+ 6.45% + Statements + 22/341 +
+ + +
+ 6.61% + Branches + 17/257 +
+ + +
+ 9.33% + Functions + 7/75 +
+ + +
+ 6.81% + Lines + 22/323 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
corslite.js +
+
0%0/370%0/230%0/130%0/37
download.js +
+
0%0/890%0/550%0/100%0/86
get-mime-type.js +
+
33.33%1/30%0/20%0/133.33%1/3
get-user-options.js +
+
80%4/550%1/2100%1/180%4/5
get-with-default.js +
+
0%0/20%0/20%0/10%0/2
has-json-structure.js +
+
0%0/70%0/40%0/10%0/6
is-empty-object.js +
+
0%0/30%0/40%0/10%0/2
is-iterable.js +
+
66.66%2/350%1/2100%1/166.66%2/3
is-latitude.js +
+
100%1/1100%2/2100%1/1100%1/1
is-longitude.js +
+
100%1/1100%2/2100%1/1100%1/1
is-model.js +
+
100%1/1100%2/2100%1/1100%1/1
is-not-model.js +
+
100%1/1100%0/0100%1/1100%1/1
is-object.js +
+
0%0/10%0/30%0/10%0/1
is-valid-coordinates.js +
+
60%6/1050%9/18100%1/160%6/10
lookup-user-ip.js +
+
0%0/550%0/320%0/100%0/54
merge-registered-items.js +
+
0%0/950%0/920%0/230%0/85
resource-view-registry.js +
+
18.51%5/270%0/120%0/720%5/25
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-empty-object.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-empty-object.js.html new file mode 100644 index 0000000..1971fe1 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-empty-object.js.html @@ -0,0 +1,103 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-empty-object.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils is-empty-object.js

+
+ +
+ 0% + Statements + 0/3 +
+ + +
+ 0% + Branches + 0/4 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/2 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7  +  +  +  +  +  + 
import { isBlank } from '@ember/utils';
+ 
+export default function isEmptyObject(obj) {
+    if (isBlank(obj)) return true;
+    return obj.constructor === Object && Object.keys(obj).length === 0;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-iterable.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-iterable.js.html new file mode 100644 index 0000000..73cb358 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-iterable.js.html @@ -0,0 +1,106 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-iterable.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils is-iterable.js

+
+ +
+ 66.66% + Statements + 2/3 +
+ + +
+ 50% + Branches + 1/2 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 66.66% + Lines + 2/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8  +3x +3x +  +  +  +  + 
export default function isIterable(obj) {
+    Eif (obj == null) {
+        return false;
+    }
+ 
+    return typeof obj[Symbol.iterator] === 'function';
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-latitude.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-latitude.js.html new file mode 100644 index 0000000..0d7d702 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-latitude.js.html @@ -0,0 +1,94 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-latitude.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils is-latitude.js

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 2/2 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4  +21x +  + 
export default function isLatitude(coordinate) {
+    return isFinite(coordinate) && Math.abs(coordinate) <= 90;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-longitude.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-longitude.js.html new file mode 100644 index 0000000..b491f00 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-longitude.js.html @@ -0,0 +1,94 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-longitude.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils is-longitude.js

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 2/2 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4  +16x +  + 
export default function isLongitude(coordinate) {
+    return isFinite(coordinate) && Math.abs(coordinate) <= 180;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-model.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-model.js.html new file mode 100644 index 0000000..e8c5a5e --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-model.js.html @@ -0,0 +1,103 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-model.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils is-model.js

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 2/2 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7  +  +  +  +7x +  + 
import Model from '@ember-data/model';
+import ObjectProxy from '@ember/object/proxy';
+ 
+export default function isModel(mixed) {
+    return mixed instanceof Model || mixed instanceof ObjectProxy;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-not-model.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-not-model.js.html new file mode 100644 index 0000000..c434eb4 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-not-model.js.html @@ -0,0 +1,100 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-not-model.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils is-not-model.js

+
+ +
+ 100% + Statements + 1/1 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 1/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6  +  +  +7x +  + 
import isModel from './is-model';
+ 
+export default function isNotModel(model) {
+    return !isModel(model);
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-object.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-object.js.html new file mode 100644 index 0000000..34f8ab6 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-object.js.html @@ -0,0 +1,94 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-object.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils is-object.js

+
+ +
+ 0% + Statements + 0/1 +
+ + +
+ 0% + Branches + 0/3 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/1 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4  +  +  + 
export default function isObject(obj) {
+    return obj && typeof obj === 'object' && Object.prototype.toString.call(obj) === '[object Object]';
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-valid-coordinates.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-valid-coordinates.js.html new file mode 100644 index 0000000..a285804 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-valid-coordinates.js.html @@ -0,0 +1,199 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/is-valid-coordinates.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils is-valid-coordinates.js

+
+ +
+ 60% + Statements + 6/10 +
+ + +
+ 50% + Branches + 9/18 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 60% + Lines + 6/10 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +21x +21x +21x +  +  +21x +  +  +  +  +21x +  +  +  +  +21x +  + 
import { isArray } from '@ember/array';
+import isLatitude from './is-latitude';
+import isLongitude from './is-longitude';
+import isObject from './is-object';
+ 
+/**
+ * Validates coordinates
+ *
+ * ```
+ * isValidCoordinates([0, 0]);
+ * isValidCoordinates({lat: 0, lng: 0});
+ * isValidCoordinates({latitude: 0, longitude: 0});
+ * isValidCoordinates(0, 0);
+ * ```
+ *
+ * @param {Array|Object|Integer} latitude
+ * @param {Array|Object|Integer} longitude
+ */
+export default function isValidCoordinates(latitude, longitude = null) {
+    let testLatitude, testLongitude;
+ 
+    Eif (isArray(latitude) && longitude === null) {
+        testLatitude = latitude[0];
+        testLongitude = latitude[1];
+    }
+ 
+    Iif (!isArray(latitude) && isObject(latitude) && longitude === null) {
+        testLatitude = latitude.lat || latitude.latitude;
+        testLongitude = latitude.lng || latitude.longitude;
+    }
+ 
+    Iif (longitude !== null) {
+        testLatitude = latitude;
+        testLongitude = longitude;
+    }
+ 
+    return isLatitude(testLatitude) && isLongitude(testLongitude);
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/lookup-user-ip.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/lookup-user-ip.js.html new file mode 100644 index 0000000..9d87ccc --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/lookup-user-ip.js.html @@ -0,0 +1,817 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/lookup-user-ip.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils lookup-user-ip.js

+
+ +
+ 0% + Statements + 0/55 +
+ + +
+ 0% + Branches + 0/32 +
+ + +
+ 0% + Functions + 0/10 +
+ + +
+ 0% + Lines + 0/54 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Lookup user's IP address and geolocation information using geoiplookup.io
+ *
+ * This method calls the geoiplookup.io API directly from the browser to get
+ * accurate user location data, avoiding the issue of server-side IP lookup
+ * returning the server's location instead of the user's location.
+ *
+ * @param {Object} options - Configuration options
+ * @param {number} options.timeout - Request timeout in milliseconds (default: 5000)
+ * @param {boolean} options.cache - Whether to cache the result (default: true)
+ * @returns {Promise<Object>} User's IP and geolocation data
+ *
+ * @example
+ * const whois = await lookupUserIp();
+ * console.log(whois.city); // "New York"
+ * console.log(whois.country_code); // "US"
+ */
+export default async function lookupUserIp(options = {}) {
+    const { timeout = 5000, cache = true } = options;
+ 
+    // Check cache first if enabled
+    if (cache) {
+        const cached = getCachedWhois();
+        if (cached) {
+            return cached;
+        }
+    }
+ 
+    // Try multiple APIs with fallback
+    const apis = [
+        {
+            url: 'https://json.geoiplookup.io/',
+            normalize: normalizeGeoIPLookup,
+        },
+        {
+            url: 'https://ipapi.co/json/',
+            normalize: normalizeIPApi,
+        },
+    ];
+ 
+    for (const api of apis) {
+        try {
+            const controller = new AbortController();
+            const timeoutId = setTimeout(() => controller.abort(), timeout);
+ 
+            const response = await fetch(api.url, {
+                signal: controller.signal,
+                headers: {
+                    Accept: 'application/json',
+                },
+            });
+ 
+            clearTimeout(timeoutId);
+ 
+            if (!response.ok) {
+                console.warn(`[lookupUserIp] ${api.url} returned ${response.status}`);
+                continue;
+            }
+ 
+            const data = await response.json();
+            const normalized = api.normalize(data);
+            const normalizedWithTimezone = ensureWhoisTimezone(normalized);
+ 
+            // Cache the result if enabled
+            if (cache) {
+                cacheWhois(normalizedWithTimezone);
+            }
+ 
+            return normalizedWithTimezone;
+        } catch (error) {
+            console.warn(`[lookupUserIp] ${api.url} failed:`, error.message);
+            // Continue to next API
+        }
+    }
+ 
+    // All APIs failed, return fallback
+    console.error('[lookupUserIp] All IP lookup APIs failed, using fallback data');
+    return getFallbackWhois();
+}
+ 
+/**
+ * Normalize geoiplookup.io response to match Fleetbase whois format
+ */
+function normalizeGeoIPLookup(data) {
+    return {
+        ip: data.ip,
+        city: data.city,
+        region: data.region,
+        country_code: data.country_code,
+        country_name: data.country_name,
+        continent_code: data.continent_code,
+        continent_name: data.continent_name,
+        latitude: data.latitude,
+        longitude: data.longitude,
+        postal_code: data.postal_code,
+        timezone: data.timezone_name || getBrowserTimezone(),
+        currency: {
+            code: data.currency_code,
+            name: data.currency_name,
+        },
+        languages: [
+            {
+                code: data.language_code,
+                name: data.language_name,
+            },
+        ],
+        isp: data.isp,
+        org: data.org,
+        asn: data.asn,
+        connection_type: data.connection_type,
+        // Metadata
+        _source: 'geoiplookup.io',
+        _timestamp: Date.now(),
+    };
+}
+ 
+/**
+ * Normalize ipapi.co response to match Fleetbase whois format
+ */
+function normalizeIPApi(data) {
+    return {
+        ip: data.ip,
+        city: data.city,
+        region: data.region,
+        country_code: data.country_code,
+        country_name: data.country_name,
+        continent_code: data.continent_code,
+        continent_name: null,
+        latitude: data.latitude,
+        longitude: data.longitude,
+        postal_code: data.postal,
+        timezone: data.timezone || getBrowserTimezone(),
+        currency: {
+            code: data.currency,
+            name: data.currency_name,
+        },
+        languages: data.languages ? data.languages.split(',').map((lang) => ({ code: lang.trim(), name: lang.trim() })) : [],
+        isp: data.org,
+        org: data.org,
+        asn: data.asn,
+        connection_type: null,
+        // Metadata
+        _source: 'ipapi.co',
+        _timestamp: Date.now(),
+    };
+}
+ 
+/**
+ * Get cached whois data from localStorage
+ */
+function getCachedWhois() {
+    try {
+        const cached = localStorage.getItem('fleetbase:whois');
+        if (!cached) {
+            return null;
+        }
+ 
+        const data = JSON.parse(cached);
+        const age = Date.now() - data._timestamp;
+        const maxAge = 60 * 60 * 1000; // 1 hour
+ 
+        if (age > maxAge) {
+            localStorage.removeItem('fleetbase:whois');
+            return null;
+        }
+ 
+        const normalized = ensureWhoisTimezone(data);
+        if (normalized.timezone !== data.timezone) {
+            cacheWhois(normalized);
+        }
+ 
+        return normalized;
+    } catch (error) {
+        console.error('[getCachedWhois] Error reading cache:', error);
+        return null;
+    }
+}
+ 
+/**
+ * Cache whois data to localStorage
+ */
+function cacheWhois(data) {
+    try {
+        localStorage.setItem('fleetbase:whois', JSON.stringify(data));
+    } catch (error) {
+        console.error('[cacheWhois] Error writing cache:', error);
+    }
+}
+ 
+/**
+ * Get fallback whois data when all APIs fail
+ */
+function getFallbackWhois() {
+    // Try to get browser language and timezone as fallback
+    const browserLang = navigator.language || 'en-US';
+    const timezone = getBrowserTimezone();
+ 
+    return {
+        ip: null,
+        city: null,
+        region: null,
+        country_code: null,
+        country_name: null,
+        continent_code: null,
+        continent_name: null,
+        latitude: null,
+        longitude: null,
+        postal_code: null,
+        timezone: timezone,
+        currency: {
+            code: null,
+            name: null,
+        },
+        languages: [
+            {
+                code: browserLang.split('-')[0],
+                name: browserLang,
+            },
+        ],
+        isp: null,
+        org: null,
+        asn: null,
+        connection_type: null,
+        _source: 'fallback',
+        _timestamp: Date.now(),
+    };
+}
+ 
+export function getBrowserTimezone() {
+    try {
+        return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
+    } catch (error) {
+        return null;
+    }
+}
+ 
+export function ensureWhoisTimezone(whois = {}) {
+    whois = whois || {};
+ 
+    return {
+        ...whois,
+        timezone: whois.timezone || getBrowserTimezone(),
+    };
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/merge-registered-items.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/merge-registered-items.js.html new file mode 100644 index 0000000..9fd8be9 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/merge-registered-items.js.html @@ -0,0 +1,676 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/merge-registered-items.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils merge-registered-items.js

+
+ +
+ 0% + Statements + 0/95 +
+ + +
+ 0% + Branches + 0/92 +
+ + +
+ 0% + Functions + 0/23 +
+ + +
+ 0% + Lines + 0/85 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { debug } from '@ember/debug';
+ 
+/**
+ * Merge the items extensions registered into a view slot with the view's own
+ * built-in items.
+ *
+ * - Built-ins win: a registered item whose `id` matches a built-in is dropped.
+ * - Registered items are placed in `priority` order (lowest first), each at its
+ *   `before`/`after` anchor, else at `index`, else at the slot's default spot:
+ *   columns go just before the row-actions column, `row-actions` and `menu`
+ *   items go before the delete action, and everything else is appended.
+ * - Every registered item is a fresh copy, so a consumer that mutates what it
+ *   renders (the column picker sets `hidden`) never touches the registry.
+ * - Handlers are bound to the view context: an action's `fn(target)` and
+ *   `isVisible(target)` become `fn(target, ctx)` and `isVisible(target, ctx)`, a
+ *   bulk action's target is the selection at click time (`ctx.getSelectedRows()`), a
+ *   button's `onClick()` becomes `onClick(ctx)`, and a button's `isVisible(ctx)`
+ *   is evaluated here, so hidden buttons are left out entirely.
+ *
+ * Pure: neither input is modified.
+ *
+ * @param {Array} baseItems The view's built-in items
+ * @param {Array} registeredItems The items registered for this slot
+ * @param {Object} options
+ * @param {String} options.slot The slot being merged (`columns`, `row-actions`, …)
+ * @param {Object} options.context The view context passed to handlers
+ * @param {Function} options.canAccess `(permission) => Boolean`; registered columns failing it are left out
+ * @returns {Array}
+ */
+export default function mergeRegisteredItems(baseItems = [], registeredItems = [], { slot = null, context = {}, canAccess = null } = {}) {
+    const result = [...toArray(baseItems)];
+    const takenIds = new Set(result.map((item) => item?.id).filter(Boolean));
+ 
+    const registered = toArray(registeredItems)
+        .map((item, order) => ({ item, order }))
+        .sort((a, b) => priorityOf(a.item) - priorityOf(b.item) || a.order - b.order)
+        .map(({ item }) => item);
+ 
+    for (const item of registered) {
+        if (!item || !item.id) {
+            continue;
+        }
+ 
+        if (takenIds.has(item.id)) {
+            debug(`[resource-view] "${item.id}" is already in the ${slot ?? 'view'} slot; the registered item was skipped.`);
+            continue;
+        }
+ 
+        if (slot === 'columns' && item.permission && typeof canAccess === 'function' && !canAccess(item.permission)) {
+            continue;
+        }
+ 
+        const prepared = prepareItem(item, slot, context);
+        if (!prepared) {
+            continue;
+        }
+ 
+        result.splice(placementIndex(result, prepared, slot), 0, prepared);
+        takenIds.add(item.id);
+    }
+ 
+    return result;
+}
+ 
+function toArray(items) {
+    if (!items) {
+        return [];
+    }
+ 
+    return typeof items.toArray === 'function' ? items.toArray() : Array.from(items);
+}
+ 
+function priorityOf(item) {
+    const priority = Number(item?.priority);
+    return Number.isFinite(priority) ? priority : 10;
+}
+ 
+function bindAction(action, context) {
+    const bound = { ...action };
+ 
+    if (typeof action.fn === 'function') {
+        bound.fn = (target, ...rest) => action.fn(target, context, ...rest);
+    }
+ 
+    if (typeof action.isVisible === 'function') {
+        bound.isVisible = (target) => action.isVisible(target, context);
+    }
+ 
+    return bound;
+}
+ 
+function prepareItem(item, slot, context) {
+    if (slot === 'actions') {
+        if (typeof item.isVisible === 'function' && !item.isVisible(context)) {
+            return null;
+        }
+ 
+        const button = { ...item };
+        delete button.isVisible;
+ 
+        if (typeof item.onClick === 'function') {
+            button.onClick = (...args) => item.onClick(context, ...args);
+        }
+ 
+        if (typeof item.fn === 'function') {
+            button.fn = (...args) => item.fn(context, ...args);
+        }
+ 
+        if (Array.isArray(item.items)) {
+            button.items = item.items
+                .filter((child) => typeof child?.isVisible !== 'function' || child.isVisible(context.resource, context))
+                .map((child) => {
+                    const bound = bindAction(child, context);
+                    if (typeof child.fn === 'function') {
+                        bound.fn = (...args) => child.fn(context.resource, context, ...args);
+                    }
+                    return bound;
+                });
+        }
+ 
+        return button;
+    }
+ 
+    if (slot === 'menu') {
+        if (typeof item.isVisible === 'function' && !item.isVisible(context.resource, context)) {
+            return null;
+        }
+ 
+        const bound = bindAction(item, context);
+        delete bound.isVisible;
+        if (typeof item.fn === 'function') {
+            bound.fn = (...args) => item.fn(context.resource, context, ...args);
+        }
+ 
+        return bound;
+    }
+ 
+    if (slot === 'row-actions') {
+        return bindAction(item, context);
+    }
+ 
+    if (slot === 'bulk-actions') {
+        const bound = bindAction(item, context);
+        if (typeof item.fn === 'function' && typeof context.getSelectedRows === 'function') {
+            bound.fn = (...args) => item.fn(context.getSelectedRows(), context, ...args);
+        }
+ 
+        return bound;
+    }
+ 
+    return { ...item };
+}
+ 
+function indexOfId(items, id) {
+    return items.findIndex((candidate) => candidate?.id === id);
+}
+ 
+function placementIndex(items, item, slot) {
+    if (item.before) {
+        const index = indexOfId(items, item.before);
+        if (index !== -1) {
+            return index;
+        }
+    }
+ 
+    if (item.after) {
+        const index = indexOfId(items, item.after);
+        if (index !== -1) {
+            return index + 1;
+        }
+    }
+ 
+    if (Number.isInteger(item.index)) {
+        return Math.min(Math.max(item.index, 0), items.length);
+    }
+ 
+    return defaultIndex(items, slot);
+}
+ 
+function defaultIndex(items, slot) {
+    if (slot === 'columns') {
+        const index = items.findIndex((column) => column?.cellComponent === 'table/cell/dropdown' || column?.sticky === 'right');
+        return index === -1 ? items.length : index;
+    }
+ 
+    if (slot === 'row-actions' || slot === 'menu') {
+        const index = indexOfId(items, 'delete');
+        if (index === -1) {
+            return items.length;
+        }
+ 
+        // Keep a separator that introduces the delete action attached to it.
+        return index > 0 && items[index - 1]?.separator ? index - 1 : index;
+    }
+ 
+    return items.length;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/resource-view-registry.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/resource-view-registry.js.html new file mode 100644 index 0000000..c348fb6 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/resource-view-registry.js.html @@ -0,0 +1,466 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/resource-view-registry.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils resource-view-registry.js

+
+ +
+ 18.51% + Statements + 5/27 +
+ + +
+ 0% + Branches + 0/12 +
+ + +
+ 0% + Functions + 0/7 +
+ + +
+ 20% + Lines + 5/25 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
/**
+ * Naming rules for the resource view registries.
+ *
+ * Every table and details view slot an extension can add to is named
+ *
+ *     <extension>:<resource>:<surface>:<slot>
+ *
+ * - `extension` is the engine that owns the view, and always equals its
+ *   console mount segment (`console.fleet-ops` → `fleet-ops`).
+ * - `resource` is the singular, dasherized resource without an engine prefix
+ *   (`invoice`, not `ledger-invoice`).
+ * - `surface` is `table` or `details`.
+ * - `slot` comes from a fixed vocabulary per surface. `actions` always means
+ *   header buttons; dropdown items are `row-actions`, `bulk-actions` or `menu`.
+ *
+ * Details tabs predate this convention and live in
+ * `<extension>:component:<resource>:details`. `<extension>:<resource>:details:tabs`
+ * is an alias for that registry: both names reach the same tabs.
+ *
+ * @example
+ * 'fleet-ops:driver:table:columns'
+ * 'ledger:invoice:details:menu'
+ * 'fleet-ops:driver:details:tabs' // → fleet-ops:component:driver:details
+ */
+ 
+export const SURFACE_SLOTS = {
+    table: ['columns', 'row-actions', 'bulk-actions', 'actions'],
+    details: ['actions', 'menu', 'tabs'],
+};
+ 
+/** The registry list each slot stores into, named after the contract it holds. */
+export const SLOT_LISTS = {
+    columns: 'table-column',
+    'row-actions': 'action',
+    'bulk-actions': 'action',
+    menu: 'action',
+    actions: 'action-button',
+    tabs: 'menu-item',
+};
+ 
+const SEGMENT = '[a-z0-9]+(?:-[a-z0-9]+)*';
+const NAME_PATTERN = new RegExp(`^(${SEGMENT}):(${SEGMENT}):(table|details):([a-z]+(?:-[a-z]+)*)$`);
+const PREFIX_PATTERN = new RegExp(`^(${SEGMENT}):(${SEGMENT}):(table|details)$`);
+ 
+/**
+ * Parse a full registry name. Returns `null` for anything that does not follow
+ * the convention, including an unknown surface or a slot the surface lacks.
+ *
+ * `section` and `list` say where the slot's items are stored. For every slot
+ * but `tabs` that is the name itself; tabs are stored in the legacy tab registry.
+ *
+ * @param {String} name
+ * @returns {{name: String, prefix: String, extension: String, resource: String, surface: String, slot: String, section: String, list: String}|null}
+ */
+export function parseRegistryName(name) {
+    if (typeof name !== 'string') {
+        return null;
+    }
+ 
+    const match = name.match(NAME_PATTERN);
+    if (!match) {
+        return null;
+    }
+ 
+    const [, extension, resource, surface, slot] = match;
+    if (!SURFACE_SLOTS[surface].includes(slot)) {
+        return null;
+    }
+ 
+    const section = slot === 'tabs' ? detailsTabsRegistryName(extension, resource) : name;
+    return { name, prefix: `${extension}:${resource}:${surface}`, extension, resource, surface, slot, section, list: SLOT_LISTS[slot] };
+}
+ 
+/**
+ * Parse a surface prefix such as `fleet-ops:driver:table`, the form layout
+ * components take as `@registry`.
+ *
+ * @param {String} prefix
+ * @returns {{prefix: String, extension: String, resource: String, surface: String}|null}
+ */
+export function parseRegistryPrefix(prefix) {
+    if (typeof prefix !== 'string') {
+        return null;
+    }
+ 
+    const match = prefix.match(PREFIX_PATTERN);
+    if (!match) {
+        return null;
+    }
+ 
+    const [, extension, resource, surface] = match;
+    return { prefix, extension, resource, surface };
+}
+ 
+/**
+ * Build a full registry name from its segments, in the order they appear.
+ *
+ * @returns {String}
+ */
+export function buildRegistryName(extension, resource, surface, slot) {
+    return `${extension}:${resource}:${surface}:${slot}`;
+}
+ 
+/**
+ * Every registry name for one resource's table and details views, including
+ * the details tabs alias.
+ *
+ * @param {String} extension
+ * @param {String} resource
+ * @returns {Array<String>}
+ */
+export function resourceRegistryNames(extension, resource) {
+    return Object.entries(SURFACE_SLOTS).flatMap(([surface, slots]) => slots.map((slot) => buildRegistryName(extension, resource, surface, slot)));
+}
+ 
+/**
+ * The legacy details-tab registry for a resource. Tabs predate the convention
+ * and keep their original name so existing registrations keep working;
+ * `<extension>:<resource>:details:tabs` is an alias for it.
+ *
+ * @returns {String}
+ */
+export function detailsTabsRegistryName(extension, resource) {
+    return `${extension}:component:${resource}:details`;
+}
+ 
+export default parseRegistryName;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize/index.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize/index.html new file mode 100644 index 0000000..510a692 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize + + + + + + + + + +
+
+

All files ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize

+
+ +
+ 88.23% + Statements + 15/17 +
+ + +
+ 66.66% + Branches + 12/18 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 87.5% + Lines + 14/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
normalize-polymorphic-type-within-hash.js +
+
90.9%10/1176.92%10/13100%1/190.9%10/11
normalize-polymorphic-type.js +
+
83.33%5/640%2/5100%2/280%4/5
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize/normalize-polymorphic-type-within-hash.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize/normalize-polymorphic-type-within-hash.js.html new file mode 100644 index 0000000..2116255 --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize/normalize-polymorphic-type-within-hash.js.html @@ -0,0 +1,166 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize/normalize-polymorphic-type-within-hash.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize normalize-polymorphic-type-within-hash.js

+
+ +
+ 90.9% + Statements + 10/11 +
+ + +
+ 76.92% + Branches + 10/13 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 90.9% + Lines + 10/11 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28  +  +  +  +  +161x +  +  +  +161x +469x +7x +7x +  +7x +  +  +7x +7x +7x +  +  +  +  +  +161x +  + 
import normalizePolymorphicType from './normalize-polymorphic-type';
+import { isBlank } from '@ember/utils';
+import { set } from '@ember/object';
+ 
+export default function serializeNormalizePolymorphicTypeWithinHash(hash) {
+    Iif (typeof hash !== 'object') {
+        return hash;
+    }
+ 
+    for (let attr in hash) {
+        if (typeof attr === 'string' && attr.includes('_type') && encodeURI(hash[attr]).includes('%5C')) {
+            const emberPolymorphicType = normalizePolymorphicType(hash[attr]);
+            const polymorphicRelationType = attr.replace(`_type`, ``);
+ 
+            hash[attr] = emberPolymorphicType;
+ 
+            // set the type of relationship using the polymorphic relation attr
+            Eif (hash[polymorphicRelationType] && !isBlank(hash[polymorphicRelationType])) {
+                Eif (hash[polymorphicRelationType].type) {
+                    set(hash, `${polymorphicRelationType}._type`, hash[polymorphicRelationType].type);
+                }
+            }
+        }
+    }
+ 
+    return hash;
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize/normalize-polymorphic-type.js.html b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize/normalize-polymorphic-type.js.html new file mode 100644 index 0000000..695538d --- /dev/null +++ b/console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize/normalize-polymorphic-type.js.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize/normalize-polymorphic-type.js + + + + + + + + + +
+
+

All files / ../../console/node_modules/.pnpm/@fleetbase+ember-core@0.3.25_@ember+string@3.1.1_@ember+test-helpers@3.3.1_@babel+core@_cb240661001c0e719c8b5c498e670661/node_modules/@fleetbase/ember-core/addon/utils/serialize normalize-polymorphic-type.js

+
+ +
+ 83.33% + Statements + 5/6 +
+ + +
+ 40% + Branches + 2/5 +
+ + +
+ 100% + Functions + 2/2 +
+ + +
+ 80% + Lines + 4/5 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +147x +  +  +  +7x +  +7x +  +  +  +  +7x +  + 
const lowercase = (string) => string.toLowerCase();
+ 
+export default function serializeNormalizePolymorphicType(polymorphicTypeValue, defaultValue = null) {
+    // encode first
+    const encodedPolymorphicTypeValue = encodeURI(polymorphicTypeValue);
+    // if encoded is invalid send the default value or the original value back
+    Iif (!encodedPolymorphicTypeValue.includes('%5C')) {
+        return defaultValue || polymorphicTypeValue;
+    }
+    // from fleetbase api should be something like Fleetbase\Models\Model
+    // change value to ember-data polymorphic type
+    return lowercase(encodedPolymorphicTypeValue.split('%5C').reverse()[0]);
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/tests/unit/utils/resource-descriptors/helpers-test.js b/tests/unit/utils/resource-descriptors/helpers-test.js index b633d50..8ccb2b4 100644 --- a/tests/unit/utils/resource-descriptors/helpers-test.js +++ b/tests/unit/utils/resource-descriptors/helpers-test.js @@ -109,7 +109,9 @@ module('Unit | Utility | resource-descriptors/helpers', function (hooks) { assert.strictEqual(typeLabel(''), null); assert.strictEqual(typeLabel('customer-fleet-ops:contact'), 'Customer'); assert.strictEqual(typeLabel('facilitator_vendor'), 'Facilitator vendor'); - assert.strictEqual(typeLabel(':contact'), null, 'nothing readable before the colon'); + assert.strictEqual(typeLabel('fleet-ops:contact'), 'Contact', 'a bare extension prefix reads as the type after it'); + assert.strictEqual(typeLabel(':contact'), 'Contact'); + assert.strictEqual(typeLabel(':'), null, 'nothing readable on either side'); }); test('money and join leave out what is missing', function (assert) { @@ -140,6 +142,20 @@ module('Unit | Utility | resource-descriptors/helpers', function (hooks) { assert.strictEqual(extensionManager({ lookup: () => null }), null); assert.strictEqual(extensionManager({ lookup: (name) => (name === 'service:universe/extension-manager' ? manager : null) }), manager); assert.strictEqual(extensionManager({ lookup: (name) => (name === 'service:universe' ? { extensionManager: manager } : null) }), manager); + assert.strictEqual( + extensionManager({ + lookup: (name) => + name === 'service:universe' + ? { + get extensionManager() { + throw new Error('no manager on this host'); + }, + } + : null, + }), + null, + 'a universe whose manager lookup throws counts as no manager' + ); }); test('engineInstalled is false without a manager, when not installed, or when the check throws', function (assert) { From c4d843f4a855600f2d0611ad40ebbdfc66200968 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Sat, 10 Oct 2026 16:40:14 +0800 Subject: [PATCH 07/10] fix(wrappers): ship the identity wrappers as app-tree templates and drop the engine keyword In the console build the addon-tree hbs-only wrappers were never compiled: ember-cli-htmlbars only colocates an addon's templates when it finds ember-cli-babel among that addon's child addons, and fleetops-data (a plain addon flagged ember-engine, shared by the host and two lazy engines) lost that lookup, so every .js module shipped while all 32 wrapper components vanished and engines failed with 'Could not find module .../components/cell/vehicle-identity'. App-tree templates are compiled by each consumer (host, fleetops, storefront) in its own namespace, which is what the wrappers are for. --- {addon => app}/components/cell/contact-identity.hbs | 0 app/components/cell/contact-identity.js | 1 - {addon => app}/components/cell/customer-identity.hbs | 0 app/components/cell/customer-identity.js | 1 - {addon => app}/components/cell/driver-identity.hbs | 0 app/components/cell/driver-identity.js | 1 - {addon => app}/components/cell/fleet-identity.hbs | 0 app/components/cell/fleet-identity.js | 1 - {addon => app}/components/cell/order-identity.hbs | 0 app/components/cell/order-identity.js | 1 - {addon => app}/components/cell/place-identity.hbs | 0 app/components/cell/place-identity.js | 1 - {addon => app}/components/cell/vehicle-identity.hbs | 0 app/components/cell/vehicle-identity.js | 1 - {addon => app}/components/cell/vendor-identity.hbs | 0 app/components/cell/vendor-identity.js | 1 - {addon => app}/components/contact/pill.hbs | 0 app/components/contact/pill.js | 1 - {addon => app}/components/contact/summary.hbs | 0 app/components/contact/summary.js | 1 - {addon => app}/components/customer/pill.hbs | 0 app/components/customer/pill.js | 1 - {addon => app}/components/customer/summary.hbs | 0 app/components/customer/summary.js | 1 - {addon => app}/components/driver/pill.hbs | 0 app/components/driver/pill.js | 1 - {addon => app}/components/driver/summary.hbs | 0 app/components/driver/summary.js | 1 - {addon => app}/components/fleet/pill.hbs | 0 app/components/fleet/pill.js | 1 - {addon => app}/components/fleet/summary.hbs | 0 app/components/fleet/summary.js | 1 - {addon => app}/components/order/pill.hbs | 0 app/components/order/pill.js | 1 - {addon => app}/components/order/summary.hbs | 0 app/components/order/summary.js | 1 - {addon => app}/components/place/pill.hbs | 0 app/components/place/pill.js | 1 - {addon => app}/components/place/summary.hbs | 0 app/components/place/summary.js | 1 - {addon => app}/components/select-option/contact.hbs | 0 app/components/select-option/contact.js | 1 - {addon => app}/components/select-option/customer.hbs | 0 app/components/select-option/customer.js | 1 - {addon => app}/components/select-option/driver.hbs | 0 app/components/select-option/driver.js | 1 - {addon => app}/components/select-option/fleet.hbs | 0 app/components/select-option/fleet.js | 1 - {addon => app}/components/select-option/order.hbs | 0 app/components/select-option/order.js | 1 - {addon => app}/components/select-option/place.hbs | 0 app/components/select-option/place.js | 1 - {addon => app}/components/select-option/vehicle.hbs | 0 app/components/select-option/vehicle.js | 1 - {addon => app}/components/select-option/vendor.hbs | 0 app/components/select-option/vendor.js | 1 - {addon => app}/components/vehicle/pill.hbs | 0 app/components/vehicle/pill.js | 1 - {addon => app}/components/vehicle/summary.hbs | 0 app/components/vehicle/summary.js | 1 - {addon => app}/components/vendor/pill.hbs | 0 app/components/vendor/pill.js | 1 - {addon => app}/components/vendor/summary.hbs | 0 app/components/vendor/summary.js | 1 - package.json | 3 +-- 65 files changed, 1 insertion(+), 34 deletions(-) rename {addon => app}/components/cell/contact-identity.hbs (100%) delete mode 100644 app/components/cell/contact-identity.js rename {addon => app}/components/cell/customer-identity.hbs (100%) delete mode 100644 app/components/cell/customer-identity.js rename {addon => app}/components/cell/driver-identity.hbs (100%) delete mode 100644 app/components/cell/driver-identity.js rename {addon => app}/components/cell/fleet-identity.hbs (100%) delete mode 100644 app/components/cell/fleet-identity.js rename {addon => app}/components/cell/order-identity.hbs (100%) delete mode 100644 app/components/cell/order-identity.js rename {addon => app}/components/cell/place-identity.hbs (100%) delete mode 100644 app/components/cell/place-identity.js rename {addon => app}/components/cell/vehicle-identity.hbs (100%) delete mode 100644 app/components/cell/vehicle-identity.js rename {addon => app}/components/cell/vendor-identity.hbs (100%) delete mode 100644 app/components/cell/vendor-identity.js rename {addon => app}/components/contact/pill.hbs (100%) delete mode 100644 app/components/contact/pill.js rename {addon => app}/components/contact/summary.hbs (100%) delete mode 100644 app/components/contact/summary.js rename {addon => app}/components/customer/pill.hbs (100%) delete mode 100644 app/components/customer/pill.js rename {addon => app}/components/customer/summary.hbs (100%) delete mode 100644 app/components/customer/summary.js rename {addon => app}/components/driver/pill.hbs (100%) delete mode 100644 app/components/driver/pill.js rename {addon => app}/components/driver/summary.hbs (100%) delete mode 100644 app/components/driver/summary.js rename {addon => app}/components/fleet/pill.hbs (100%) delete mode 100644 app/components/fleet/pill.js rename {addon => app}/components/fleet/summary.hbs (100%) delete mode 100644 app/components/fleet/summary.js rename {addon => app}/components/order/pill.hbs (100%) delete mode 100644 app/components/order/pill.js rename {addon => app}/components/order/summary.hbs (100%) delete mode 100644 app/components/order/summary.js rename {addon => app}/components/place/pill.hbs (100%) delete mode 100644 app/components/place/pill.js rename {addon => app}/components/place/summary.hbs (100%) delete mode 100644 app/components/place/summary.js rename {addon => app}/components/select-option/contact.hbs (100%) delete mode 100644 app/components/select-option/contact.js rename {addon => app}/components/select-option/customer.hbs (100%) delete mode 100644 app/components/select-option/customer.js rename {addon => app}/components/select-option/driver.hbs (100%) delete mode 100644 app/components/select-option/driver.js rename {addon => app}/components/select-option/fleet.hbs (100%) delete mode 100644 app/components/select-option/fleet.js rename {addon => app}/components/select-option/order.hbs (100%) delete mode 100644 app/components/select-option/order.js rename {addon => app}/components/select-option/place.hbs (100%) delete mode 100644 app/components/select-option/place.js rename {addon => app}/components/select-option/vehicle.hbs (100%) delete mode 100644 app/components/select-option/vehicle.js rename {addon => app}/components/select-option/vendor.hbs (100%) delete mode 100644 app/components/select-option/vendor.js rename {addon => app}/components/vehicle/pill.hbs (100%) delete mode 100644 app/components/vehicle/pill.js rename {addon => app}/components/vehicle/summary.hbs (100%) delete mode 100644 app/components/vehicle/summary.js rename {addon => app}/components/vendor/pill.hbs (100%) delete mode 100644 app/components/vendor/pill.js rename {addon => app}/components/vendor/summary.hbs (100%) delete mode 100644 app/components/vendor/summary.js diff --git a/addon/components/cell/contact-identity.hbs b/app/components/cell/contact-identity.hbs similarity index 100% rename from addon/components/cell/contact-identity.hbs rename to app/components/cell/contact-identity.hbs diff --git a/app/components/cell/contact-identity.js b/app/components/cell/contact-identity.js deleted file mode 100644 index 3a97c2a..0000000 --- a/app/components/cell/contact-identity.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/cell/contact-identity'; diff --git a/addon/components/cell/customer-identity.hbs b/app/components/cell/customer-identity.hbs similarity index 100% rename from addon/components/cell/customer-identity.hbs rename to app/components/cell/customer-identity.hbs diff --git a/app/components/cell/customer-identity.js b/app/components/cell/customer-identity.js deleted file mode 100644 index 7832618..0000000 --- a/app/components/cell/customer-identity.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/cell/customer-identity'; diff --git a/addon/components/cell/driver-identity.hbs b/app/components/cell/driver-identity.hbs similarity index 100% rename from addon/components/cell/driver-identity.hbs rename to app/components/cell/driver-identity.hbs diff --git a/app/components/cell/driver-identity.js b/app/components/cell/driver-identity.js deleted file mode 100644 index 431d0ec..0000000 --- a/app/components/cell/driver-identity.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/cell/driver-identity'; diff --git a/addon/components/cell/fleet-identity.hbs b/app/components/cell/fleet-identity.hbs similarity index 100% rename from addon/components/cell/fleet-identity.hbs rename to app/components/cell/fleet-identity.hbs diff --git a/app/components/cell/fleet-identity.js b/app/components/cell/fleet-identity.js deleted file mode 100644 index 263b22b..0000000 --- a/app/components/cell/fleet-identity.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/cell/fleet-identity'; diff --git a/addon/components/cell/order-identity.hbs b/app/components/cell/order-identity.hbs similarity index 100% rename from addon/components/cell/order-identity.hbs rename to app/components/cell/order-identity.hbs diff --git a/app/components/cell/order-identity.js b/app/components/cell/order-identity.js deleted file mode 100644 index 4bb3613..0000000 --- a/app/components/cell/order-identity.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/cell/order-identity'; diff --git a/addon/components/cell/place-identity.hbs b/app/components/cell/place-identity.hbs similarity index 100% rename from addon/components/cell/place-identity.hbs rename to app/components/cell/place-identity.hbs diff --git a/app/components/cell/place-identity.js b/app/components/cell/place-identity.js deleted file mode 100644 index eb31bd1..0000000 --- a/app/components/cell/place-identity.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/cell/place-identity'; diff --git a/addon/components/cell/vehicle-identity.hbs b/app/components/cell/vehicle-identity.hbs similarity index 100% rename from addon/components/cell/vehicle-identity.hbs rename to app/components/cell/vehicle-identity.hbs diff --git a/app/components/cell/vehicle-identity.js b/app/components/cell/vehicle-identity.js deleted file mode 100644 index 9e54d46..0000000 --- a/app/components/cell/vehicle-identity.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/cell/vehicle-identity'; diff --git a/addon/components/cell/vendor-identity.hbs b/app/components/cell/vendor-identity.hbs similarity index 100% rename from addon/components/cell/vendor-identity.hbs rename to app/components/cell/vendor-identity.hbs diff --git a/app/components/cell/vendor-identity.js b/app/components/cell/vendor-identity.js deleted file mode 100644 index 9ef5d6c..0000000 --- a/app/components/cell/vendor-identity.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/cell/vendor-identity'; diff --git a/addon/components/contact/pill.hbs b/app/components/contact/pill.hbs similarity index 100% rename from addon/components/contact/pill.hbs rename to app/components/contact/pill.hbs diff --git a/app/components/contact/pill.js b/app/components/contact/pill.js deleted file mode 100644 index c7a297a..0000000 --- a/app/components/contact/pill.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/contact/pill'; diff --git a/addon/components/contact/summary.hbs b/app/components/contact/summary.hbs similarity index 100% rename from addon/components/contact/summary.hbs rename to app/components/contact/summary.hbs diff --git a/app/components/contact/summary.js b/app/components/contact/summary.js deleted file mode 100644 index 7e7c800..0000000 --- a/app/components/contact/summary.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/contact/summary'; diff --git a/addon/components/customer/pill.hbs b/app/components/customer/pill.hbs similarity index 100% rename from addon/components/customer/pill.hbs rename to app/components/customer/pill.hbs diff --git a/app/components/customer/pill.js b/app/components/customer/pill.js deleted file mode 100644 index aea27bb..0000000 --- a/app/components/customer/pill.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/customer/pill'; diff --git a/addon/components/customer/summary.hbs b/app/components/customer/summary.hbs similarity index 100% rename from addon/components/customer/summary.hbs rename to app/components/customer/summary.hbs diff --git a/app/components/customer/summary.js b/app/components/customer/summary.js deleted file mode 100644 index 899a942..0000000 --- a/app/components/customer/summary.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/customer/summary'; diff --git a/addon/components/driver/pill.hbs b/app/components/driver/pill.hbs similarity index 100% rename from addon/components/driver/pill.hbs rename to app/components/driver/pill.hbs diff --git a/app/components/driver/pill.js b/app/components/driver/pill.js deleted file mode 100644 index 3490441..0000000 --- a/app/components/driver/pill.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/driver/pill'; diff --git a/addon/components/driver/summary.hbs b/app/components/driver/summary.hbs similarity index 100% rename from addon/components/driver/summary.hbs rename to app/components/driver/summary.hbs diff --git a/app/components/driver/summary.js b/app/components/driver/summary.js deleted file mode 100644 index e96be83..0000000 --- a/app/components/driver/summary.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/driver/summary'; diff --git a/addon/components/fleet/pill.hbs b/app/components/fleet/pill.hbs similarity index 100% rename from addon/components/fleet/pill.hbs rename to app/components/fleet/pill.hbs diff --git a/app/components/fleet/pill.js b/app/components/fleet/pill.js deleted file mode 100644 index d849910..0000000 --- a/app/components/fleet/pill.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/fleet/pill'; diff --git a/addon/components/fleet/summary.hbs b/app/components/fleet/summary.hbs similarity index 100% rename from addon/components/fleet/summary.hbs rename to app/components/fleet/summary.hbs diff --git a/app/components/fleet/summary.js b/app/components/fleet/summary.js deleted file mode 100644 index e8a90c5..0000000 --- a/app/components/fleet/summary.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/fleet/summary'; diff --git a/addon/components/order/pill.hbs b/app/components/order/pill.hbs similarity index 100% rename from addon/components/order/pill.hbs rename to app/components/order/pill.hbs diff --git a/app/components/order/pill.js b/app/components/order/pill.js deleted file mode 100644 index f3ceb03..0000000 --- a/app/components/order/pill.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/order/pill'; diff --git a/addon/components/order/summary.hbs b/app/components/order/summary.hbs similarity index 100% rename from addon/components/order/summary.hbs rename to app/components/order/summary.hbs diff --git a/app/components/order/summary.js b/app/components/order/summary.js deleted file mode 100644 index 24bf123..0000000 --- a/app/components/order/summary.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/order/summary'; diff --git a/addon/components/place/pill.hbs b/app/components/place/pill.hbs similarity index 100% rename from addon/components/place/pill.hbs rename to app/components/place/pill.hbs diff --git a/app/components/place/pill.js b/app/components/place/pill.js deleted file mode 100644 index 2757d3a..0000000 --- a/app/components/place/pill.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/place/pill'; diff --git a/addon/components/place/summary.hbs b/app/components/place/summary.hbs similarity index 100% rename from addon/components/place/summary.hbs rename to app/components/place/summary.hbs diff --git a/app/components/place/summary.js b/app/components/place/summary.js deleted file mode 100644 index 3ee08c9..0000000 --- a/app/components/place/summary.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/place/summary'; diff --git a/addon/components/select-option/contact.hbs b/app/components/select-option/contact.hbs similarity index 100% rename from addon/components/select-option/contact.hbs rename to app/components/select-option/contact.hbs diff --git a/app/components/select-option/contact.js b/app/components/select-option/contact.js deleted file mode 100644 index fd9d008..0000000 --- a/app/components/select-option/contact.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/select-option/contact'; diff --git a/addon/components/select-option/customer.hbs b/app/components/select-option/customer.hbs similarity index 100% rename from addon/components/select-option/customer.hbs rename to app/components/select-option/customer.hbs diff --git a/app/components/select-option/customer.js b/app/components/select-option/customer.js deleted file mode 100644 index 26fd3c3..0000000 --- a/app/components/select-option/customer.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/select-option/customer'; diff --git a/addon/components/select-option/driver.hbs b/app/components/select-option/driver.hbs similarity index 100% rename from addon/components/select-option/driver.hbs rename to app/components/select-option/driver.hbs diff --git a/app/components/select-option/driver.js b/app/components/select-option/driver.js deleted file mode 100644 index a08f3e0..0000000 --- a/app/components/select-option/driver.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/select-option/driver'; diff --git a/addon/components/select-option/fleet.hbs b/app/components/select-option/fleet.hbs similarity index 100% rename from addon/components/select-option/fleet.hbs rename to app/components/select-option/fleet.hbs diff --git a/app/components/select-option/fleet.js b/app/components/select-option/fleet.js deleted file mode 100644 index 3e6eba6..0000000 --- a/app/components/select-option/fleet.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/select-option/fleet'; diff --git a/addon/components/select-option/order.hbs b/app/components/select-option/order.hbs similarity index 100% rename from addon/components/select-option/order.hbs rename to app/components/select-option/order.hbs diff --git a/app/components/select-option/order.js b/app/components/select-option/order.js deleted file mode 100644 index 34dc932..0000000 --- a/app/components/select-option/order.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/select-option/order'; diff --git a/addon/components/select-option/place.hbs b/app/components/select-option/place.hbs similarity index 100% rename from addon/components/select-option/place.hbs rename to app/components/select-option/place.hbs diff --git a/app/components/select-option/place.js b/app/components/select-option/place.js deleted file mode 100644 index d78d889..0000000 --- a/app/components/select-option/place.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/select-option/place'; diff --git a/addon/components/select-option/vehicle.hbs b/app/components/select-option/vehicle.hbs similarity index 100% rename from addon/components/select-option/vehicle.hbs rename to app/components/select-option/vehicle.hbs diff --git a/app/components/select-option/vehicle.js b/app/components/select-option/vehicle.js deleted file mode 100644 index 3c49271..0000000 --- a/app/components/select-option/vehicle.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/select-option/vehicle'; diff --git a/addon/components/select-option/vendor.hbs b/app/components/select-option/vendor.hbs similarity index 100% rename from addon/components/select-option/vendor.hbs rename to app/components/select-option/vendor.hbs diff --git a/app/components/select-option/vendor.js b/app/components/select-option/vendor.js deleted file mode 100644 index da45f9c..0000000 --- a/app/components/select-option/vendor.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/select-option/vendor'; diff --git a/addon/components/vehicle/pill.hbs b/app/components/vehicle/pill.hbs similarity index 100% rename from addon/components/vehicle/pill.hbs rename to app/components/vehicle/pill.hbs diff --git a/app/components/vehicle/pill.js b/app/components/vehicle/pill.js deleted file mode 100644 index e1f78a1..0000000 --- a/app/components/vehicle/pill.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/vehicle/pill'; diff --git a/addon/components/vehicle/summary.hbs b/app/components/vehicle/summary.hbs similarity index 100% rename from addon/components/vehicle/summary.hbs rename to app/components/vehicle/summary.hbs diff --git a/app/components/vehicle/summary.js b/app/components/vehicle/summary.js deleted file mode 100644 index 5dae0f5..0000000 --- a/app/components/vehicle/summary.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/vehicle/summary'; diff --git a/addon/components/vendor/pill.hbs b/app/components/vendor/pill.hbs similarity index 100% rename from addon/components/vendor/pill.hbs rename to app/components/vendor/pill.hbs diff --git a/app/components/vendor/pill.js b/app/components/vendor/pill.js deleted file mode 100644 index fa810e5..0000000 --- a/app/components/vendor/pill.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/vendor/pill'; diff --git a/addon/components/vendor/summary.hbs b/app/components/vendor/summary.hbs similarity index 100% rename from addon/components/vendor/summary.hbs rename to app/components/vendor/summary.hbs diff --git a/app/components/vendor/summary.js b/app/components/vendor/summary.js deleted file mode 100644 index b9f4eee..0000000 --- a/app/components/vendor/summary.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@fleetbase/fleetops-data/components/vendor/summary'; diff --git a/package.json b/package.json index 365edaf..986b75c 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,7 @@ "fleetops-data", "fleetbase-fleet-ops", "fleetbase", - "ember-addon", - "ember-engine" + "ember-addon" ], "repository": "https://github.com/fleetbase/fleetops-data", "license": "AGPL-3.0-or-later", From 3e1e0dba535f3403cda747757181cb161a8bc6c8 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Mon, 12 Oct 2026 19:18:43 +0800 Subject: [PATCH 08/10] fix(descriptors): outside Fleet-Ops a driver, vehicle or other record opens its Fleet-Ops page instead of a panel whose components only Fleet-Ops can render --- addon/utils/resource-descriptors/helpers.js | 31 ++++++++++++++-- .../resource-descriptors/helpers-test.js | 35 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/addon/utils/resource-descriptors/helpers.js b/addon/utils/resource-descriptors/helpers.js index 3948d4c..ed2319e 100644 --- a/addon/utils/resource-descriptors/helpers.js +++ b/addon/utils/resource-descriptors/helpers.js @@ -162,10 +162,32 @@ export function engineInstalled(owner, engineName = FLEETOPS_ENGINE) { } } +/** + * Whether the console is on one of the engine's own routes. Unknown means yes, so a host + * without a router keeps the previous behaviour. + */ +export function isInsideEngine(manager, engineName = FLEETOPS_ENGINE) { + let current; + let mountPoint; + + try { + current = manager?.applicationInstance?.lookup?.('service:router')?.currentRouteName; + mountPoint = manager?.getEngineMountPoint?.(engineName); + } catch { + return true; + } + + if (typeof current !== 'string' || typeof mountPoint !== 'string' || mountPoint === '') { + return true; + } + + return current === mountPoint.replace(/\.$/, '') || current.startsWith(mountPoint); +} + /** * An opener that loads an engine on demand, then hands the record to one of - * its action services: the context panel when it has one, else its route - * transition. The engine bundle is only fetched when someone actually clicks, + * its action services: the context panel when it has one and the console is inside that + * engine, else its route transition. The engine bundle is only fetched when someone actually clicks, * never when a page that merely shows the resource renders. */ export function engineOpener(owner, serviceName, { engineName = FLEETOPS_ENGINE, mode = 'panel' } = {}) { @@ -190,7 +212,10 @@ export function engineOpener(owner, serviceName, { engineName = FLEETOPS_ENGINE, return false; } - const view = (mode === 'panel' ? (service.panel?.view ?? service.transition?.view) : null) ?? (mode === 'transition' ? service.transition?.view : null); + // A panel renders the engine's own components, which only resolve inside that engine's + // routes; from anywhere else (Storefront, the console home) open the record's page instead. + const panelView = mode === 'panel' && isInsideEngine(manager, engineName) ? service.panel?.view : null; + const view = panelView ?? (mode === 'panel' || mode === 'transition' ? service.transition?.view : null); if (typeof view !== 'function') { return false; diff --git a/tests/unit/utils/resource-descriptors/helpers-test.js b/tests/unit/utils/resource-descriptors/helpers-test.js index 8ccb2b4..dac9e20 100644 --- a/tests/unit/utils/resource-descriptors/helpers-test.js +++ b/tests/unit/utils/resource-descriptors/helpers-test.js @@ -19,6 +19,7 @@ import { extensionManager, engineInstalled, engineOpener, + isInsideEngine, FLEETOPS_ENGINE, } from '@fleetbase/fleetops-data/utils/resource-descriptors/helpers'; import { PLACEHOLDER_IMAGES } from '@fleetbase/fleetops-data/utils/placeholder-images'; @@ -219,5 +220,39 @@ module('Unit | Utility | resource-descriptors/helpers', function (hooks) { ['transition', record], ]); }); + + test("engineOpener opens the record's page instead of a panel outside the engine's routes", async function (assert) { + const record = { id: 'r1' }; + const seen = []; + const service = { panel: { view: () => seen.push('panel') }, transition: { view: () => seen.push('transition') } }; + const at = (routeName) => + ownerWithEngine( + { 'vehicle-actions': service, 'panel-only': { panel: { view: () => seen.push('panel-only') } } }, + { + applicationInstance: { lookup: (name) => (name === 'service:router' ? { currentRouteName: routeName } : null) }, + getEngineMountPoint: () => 'console.fleet-ops.', + } + ); + + assert.true(await engineOpener(at('console.fleet-ops.management.vehicles.index'), 'vehicle-actions')(record)); + assert.true(await engineOpener(at('console.storefront.food-trucks.index'), 'vehicle-actions')(record)); + assert.false(await engineOpener(at('console.storefront.food-trucks.index'), 'panel-only')(record), 'nothing to open outside without a transition'); + assert.deepEqual(seen, ['panel', 'transition'], 'the panel inside Fleet-Ops, the page from Storefront'); + }); + + test('isInsideEngine reads the console route against the engine mount point and assumes yes when it cannot tell', function (assert) { + const manager = (routeName, mountPoint = 'console.fleet-ops.') => ({ + applicationInstance: { lookup: () => ({ currentRouteName: routeName }) }, + getEngineMountPoint: () => mountPoint, + }); + + assert.true(isInsideEngine(manager('console.fleet-ops.operations.orders.index'))); + assert.true(isInsideEngine(manager('console.fleet-ops')), 'the engine root'); + assert.false(isInsideEngine(manager('console.storefront.index'))); + assert.false(isInsideEngine(manager('console.fleet-ops-reports.index')), 'a prefix match is not enough'); + assert.true(isInsideEngine(null), 'no manager'); + assert.true(isInsideEngine(manager(undefined)), 'no route'); + assert.true(isInsideEngine(manager('console.storefront.index', null)), 'no mount point'); + }); }); }); From d728efbfc850eed8956f83b5ca2f772b0a6dbb1e Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Mon, 12 Oct 2026 21:39:14 +0800 Subject: [PATCH 09/10] test(descriptors): cover isInsideEngine assuming inside when the extension manager throws --- tests/unit/utils/resource-descriptors/helpers-test.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/unit/utils/resource-descriptors/helpers-test.js b/tests/unit/utils/resource-descriptors/helpers-test.js index dac9e20..8ed8815 100644 --- a/tests/unit/utils/resource-descriptors/helpers-test.js +++ b/tests/unit/utils/resource-descriptors/helpers-test.js @@ -253,6 +253,15 @@ module('Unit | Utility | resource-descriptors/helpers', function (hooks) { assert.true(isInsideEngine(null), 'no manager'); assert.true(isInsideEngine(manager(undefined)), 'no route'); assert.true(isInsideEngine(manager('console.storefront.index', null)), 'no mount point'); + assert.true( + isInsideEngine({ + applicationInstance: { lookup: () => ({ currentRouteName: 'console.storefront.index' }) }, + getEngineMountPoint: () => { + throw new Error('engine not registered'); + }, + }), + 'a manager that throws' + ); }); }); }); From 550c866b3c68643608b8edccbb637a25626a3fe0 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Wed, 14 Oct 2026 12:47:06 +0800 Subject: [PATCH 10/10] docs(release): v0.2.3 notes cover the shared descriptors --- RELEASE.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index d5f7a47..4bb1f93 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,11 +1,19 @@ -> v0.2.3 ~ "Customer places load" +> v0.2.3 ~ "Shared resource identity descriptors, and customer places that load" --- ## Highlights +- **Shared resource identity descriptors.** The identity descriptors for `driver`, `vehicle`, `customer`, `contact`, `place`, `order`, `vendor` and `fleet` now live in this package, so any extension that depends on it renders those records as identity cells, pills and summaries without loading the Fleet-Ops engine. `utils/resource-descriptors/shared` builds them, `utils/resource-descriptors/helpers` holds the building blocks, and `utils/placeholder-images` is the styled placeholder set. The thin ``, ``, `` and `` wrappers for those eight keys ship as app-tree templates, so they resolve inside every consumer. An instance initializer registers the descriptors with ember-ui's resource registry on every host and engine boot, filling in only keys nobody has registered; Fleet-Ops replaces them with its own openers when it boots. ([#86](https://github.com/fleetbase/fleetops-data/pull/86)) +- **Records open on their Fleet-Ops page outside Fleet-Ops.** Opening a resource loads the Fleet-Ops engine on demand. Inside its routes the record's context panel opens; from any other engine the record opens on its Fleet-Ops page, since the panel's components only Fleet-Ops can render. ([#88](https://github.com/fleetbase/fleetops-data/pull/88)) +- **Identity stub builder.** `utils/identity-stub` (`buildIdentityStub`) moves here from Fleet-Ops, so an extension can build a stand-in record for a relation a row only knows by name. `typeLabel('fleet-ops:contact')` reads "Contact", and a host without an extension manager no longer throws from `canOpen`. ([#87](https://github.com/fleetbase/fleetops-data/pull/87)) - **An order's customer loads with its saved places.** Customer contacts and customer vendors now have their own serializers, so their embedded `place` and `places` load as `place` records. Before, a place with a type such as "apartment" or "house" was read as a model name, and the order failed to load with "No model was found for 'apartment'". +--- +## Upgrading +The shared descriptors register with the `resource-registry` service from fleetbase/ember-ui v0.4.5 or later. fleetbase/fleetops v0.6.72 builds its own driver, vehicle, customer, contact, place, order, vendor and fleet descriptors from these and re-exports the moved helpers, so an older Fleet-Ops keeps working on its own copies. + --- ## Need help? - [GitHub Discussions](https://github.com/fleetbase/fleetbase/discussions) - [Discord](https://discord.gg/HnTqQ6zAVn) +---