diff --git a/addon/contracts/menu-item.js b/addon/contracts/menu-item.js index 3579a46..f19ad6f 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'; /** @@ -123,6 +124,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 = isArray(definition.tags) ? definition.tags : definition.tags ? [definition.tags] : null; } else { // Handle string title with optional route (chaining pattern) this.title = titleOrDefinition; @@ -178,6 +184,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 @@ -388,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: + * + * 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} * - * { title, route, icon?, iconPrefix?, id? } + * Metadata: + * description {String} Short description shown in the card + * tags {String[]} Search tags * - * 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. + * 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 @@ -404,16 +437,47 @@ 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; } + /** + * 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. @@ -428,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]; @@ -502,6 +566,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 85b38c9..6d70f6c 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'; @@ -153,6 +153,70 @@ 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 (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, + 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'); } 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",