From fb33ab75f2d60a6334a8125f88fde2ea366876b0 Mon Sep 17 00:00:00 2001 From: Fleetbase Dev Date: Sun, 1 Mar 2026 00:28:58 -0500 Subject: [PATCH 1/5] feat(menu-service): auto-register shortcuts as first-class header menu items When a parent MenuItem with a shortcuts array is registered via registerHeaderMenuItem, each shortcut is now immediately registered as its own first-class header menu item with a dasherized id of the form `{parentId}-sc-{shortcutTitle}`. This fixes three related issues: 1. Shortcuts now appear in universe.headerMenuItems from boot, so the Customise Navigation panel's "All Extensions" column shows them. 2. Pinned shortcuts can be found by id in allItems when rebuilding the bar, so they correctly appear in the "Pinned to Bar" column. 3. The id is always dasherized (e.g. fleet-ops-sc-live-map) so lookups are consistent regardless of how the shortcut title is capitalised. --- addon/services/universe/menu-service.js | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/addon/services/universe/menu-service.js b/addon/services/universe/menu-service.js index 85b38c9..00b9757 100644 --- a/addon/services/universe/menu-service.js +++ b/addon/services/universe/menu-service.js @@ -153,6 +153,32 @@ export default class MenuService extends Service.extend(Evented) { 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 (Array.isArray(menuItem.shortcuts)) { + for (const sc of menuItem.shortcuts) { + const scId = sc.id ?? dasherize(menuItem.id + '-sc-' + sc.title); + const scSlug = sc.slug ?? scId; + const scItem = { + id: scId, + slug: scSlug, + title: sc.title, + route: sc.route ?? menuItem.route, + icon: sc.icon ?? menuItem.icon, + iconPrefix: sc.iconPrefix ?? menuItem.iconPrefix, + description: sc.description ?? null, + _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'); } From 8606358c82dde82e5c8e2dcda23ec886ebd48364 Mon Sep 17 00:00:00 2001 From: Fleetbase Dev Date: Sun, 1 Mar 2026 00:33:46 -0500 Subject: [PATCH 2/5] feat(menu-item): add tags property + use Ember isArray MenuItem contract: - Added `tags` property (String[] | null) to both constructor branches and toObject(). Accepts a single string or an array; normalised to an array internally. - Added `withTags(tags)` builder method for the chaining pattern. menu-service.js: - Swapped native Array.isArray for Ember's isArray (imported from `@ember/array`) in the shortcut auto-registration loop. - Shortcut scItems now inherit the parent's tags (or their own if defined) so they remain discoverable via the same search terms. --- addon/contracts/menu-item.js | 29 +++++++++++++++++++++++++ addon/services/universe/menu-service.js | 7 ++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/addon/contracts/menu-item.js b/addon/contracts/menu-item.js index 3579a46..2115d7d 100644 --- a/addon/contracts/menu-item.js +++ b/addon/contracts/menu-item.js @@ -123,6 +123,11 @@ export default class MenuItem extends BaseContract { // 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 = Array.isArray(definition.tags) ? definition.tags : (definition.tags ? [definition.tags] : null); } else { // Handle string title with optional route (chaining pattern) this.title = titleOrDefinition; @@ -178,6 +183,7 @@ export default class MenuItem extends BaseContract { // ── Phase 2 additions ────────────────────────────────────────── this.description = null; this.shortcuts = null; + this.tags = null; } // Call setup() to trigger validation after properties are set @@ -414,6 +420,26 @@ export default class MenuItem extends BaseContract { 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 = Array.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. @@ -502,6 +528,9 @@ export default class MenuItem extends BaseContract { // 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, diff --git a/addon/services/universe/menu-service.js b/addon/services/universe/menu-service.js index 00b9757..0b1839a 100644 --- a/addon/services/universe/menu-service.js +++ b/addon/services/universe/menu-service.js @@ -3,7 +3,7 @@ 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 } from '@ember/array'; +import { A, isArray } from '@ember/array'; import MenuItem from '../../contracts/menu-item'; import MenuPanel from '../../contracts/menu-panel'; @@ -156,7 +156,7 @@ export default class MenuService extends Service.extend(Evented) { // 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 (Array.isArray(menuItem.shortcuts)) { + 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; @@ -168,6 +168,9 @@ export default class MenuService extends Service.extend(Evented) { icon: sc.icon ?? menuItem.icon, iconPrefix: sc.iconPrefix ?? menuItem.iconPrefix, description: sc.description ?? null, + // Inherit parent tags and merge with any shortcut-specific tags + // so that shortcuts are discoverable via the same search terms. + tags: isArray(sc.tags) ? sc.tags : (isArray(menuItem.tags) ? menuItem.tags : null), _isShortcut: true, _parentTitle: menuItem.title, _parentId: menuItem.id, From 2b2af96f2163a5005f32273be969f04573749154 Mon Sep 17 00:00:00 2001 From: Fleetbase Dev Date: Sun, 1 Mar 2026 00:36:05 -0500 Subject: [PATCH 3/5] feat(menu-item): use Ember isArray + full shortcut property surface menu-item.js: - Import isArray from `@ember/array` and replace all four Array.isArray calls (tags normalisation in constructor, withShortcuts, withTags, addShortcut guard). menu-service.js: - Expanded scItem builder to forward the complete MenuItem property surface for each shortcut: identity, routing, full icon properties (icon, iconPrefix, iconSize, iconClass, iconComponent, iconComponentOptions), metadata (description, tags), behaviour (onClick, disabled, type, buttonType), and styling (class, inlineClass, wrapperClass). --- addon/contracts/menu-item.js | 62 ++++++++++++++++++++----- addon/services/universe/menu-service.js | 39 +++++++++++++++- 2 files changed, 87 insertions(+), 14 deletions(-) diff --git a/addon/contracts/menu-item.js b/addon/contracts/menu-item.js index 2115d7d..ed60fa9 100644 --- a/addon/contracts/menu-item.js +++ b/addon/contracts/menu-item.js @@ -1,6 +1,7 @@ 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'; /** @@ -127,7 +128,7 @@ export default class MenuItem extends BaseContract { // 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 = Array.isArray(definition.tags) ? definition.tags : (definition.tags ? [definition.tags] : 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; @@ -394,14 +395,40 @@ export default class MenuItem extends BaseContract { } /** - * Set an array of shortcut items displayed beneath the extension in the - * multi-column overflow dropdown. Each shortcut is a plain object: + * 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: * - * { title, route, icon?, iconPrefix?, id? } + * Required: + * title {String} Display label * - * Shortcuts are purely navigational – they do not support onClick handlers. - * They are rendered as compact links inside the extension card in the - * dropdown and can be individually pinned to the navigation bar. + * 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} shortcuts Array of shortcut definition objects @@ -410,12 +437,23 @@ export default class MenuItem extends BaseContract { * @example * new MenuItem('Fleet-Ops', 'console.fleet-ops') * .withShortcuts([ - * { title: 'Scheduler', route: 'console.fleet-ops.scheduler', icon: 'calendar' }, - * { title: 'Order Config', route: 'console.fleet-ops.order-configs', icon: 'gear' }, + * { + * 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 = Array.isArray(shortcuts) ? shortcuts : null; + this.shortcuts = isArray(shortcuts) ? shortcuts : null; this._options.shortcuts = this.shortcuts; return this; } @@ -435,7 +473,7 @@ export default class MenuItem extends BaseContract { * .withTags(['logistics', 'tracking', 'fleet', 'drivers']) */ withTags(tags) { - this.tags = Array.isArray(tags) ? tags : (tags ? [tags] : null); + this.tags = isArray(tags) ? tags : (tags ? [tags] : null); this._options.tags = this.tags; return this; } @@ -454,7 +492,7 @@ export default class MenuItem extends BaseContract { * .addShortcut({ title: 'Order Config', route: 'console.fleet-ops.order-configs' }) */ addShortcut(shortcut) { - if (!Array.isArray(this.shortcuts)) { + if (!isArray(this.shortcuts)) { this.shortcuts = []; } this.shortcuts = [...this.shortcuts, shortcut]; diff --git a/addon/services/universe/menu-service.js b/addon/services/universe/menu-service.js index 0b1839a..5006c96 100644 --- a/addon/services/universe/menu-service.js +++ b/addon/services/universe/menu-service.js @@ -160,17 +160,52 @@ export default class MenuService extends Service.extend(Evented) { 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, - // Inherit parent tags and merge with any shortcut-specific tags - // so that shortcuts are discoverable via the same search terms. + // 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, + 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, From f2ad03d71847d472cdd40c508a13ccce9fa74bdf Mon Sep 17 00:00:00 2001 From: Fleetbase Dev Date: Sun, 1 Mar 2026 00:37:46 -0500 Subject: [PATCH 4/5] fix(lint): remove redundant parentheses around ternaries (prettier) --- addon/contracts/menu-item.js | 4 ++-- addon/services/universe/menu-service.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/addon/contracts/menu-item.js b/addon/contracts/menu-item.js index ed60fa9..f19ad6f 100644 --- a/addon/contracts/menu-item.js +++ b/addon/contracts/menu-item.js @@ -128,7 +128,7 @@ export default class MenuItem extends BaseContract { // 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); + this.tags = isArray(definition.tags) ? definition.tags : definition.tags ? [definition.tags] : null; } else { // Handle string title with optional route (chaining pattern) this.title = titleOrDefinition; @@ -473,7 +473,7 @@ export default class MenuItem extends BaseContract { * .withTags(['logistics', 'tracking', 'fleet', 'drivers']) */ withTags(tags) { - this.tags = isArray(tags) ? tags : (tags ? [tags] : null); + this.tags = isArray(tags) ? tags : tags ? [tags] : null; this._options.tags = this.tags; return this; } diff --git a/addon/services/universe/menu-service.js b/addon/services/universe/menu-service.js index 5006c96..6d70f6c 100644 --- a/addon/services/universe/menu-service.js +++ b/addon/services/universe/menu-service.js @@ -192,7 +192,7 @@ export default class MenuService extends Service.extend(Evented) { 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), + tags: isArray(sc.tags) ? sc.tags : isArray(menuItem.tags) ? menuItem.tags : null, // ── Behaviour ───────────────────────────────────────────── onClick: sc.onClick ?? null, From dc4455ea1cfae5cac45ea51a047480349847f787 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Sun, 1 Mar 2026 13:55:41 +0800 Subject: [PATCH 5/5] bumped to version v0.3.14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee4ef11..55982a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fleetbase/ember-core", - "version": "0.3.13", + "version": "0.3.14", "description": "Provides all the core services, decorators and utilities for building a Fleetbase extension for the Console.", "keywords": [ "fleetbase-core",