@fleetbase/ember-core
Version:
Provides all the core services, decorators and utilities for building a Fleetbase extension for the Console.
580 lines (522 loc) • 19.5 kB
JavaScript
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'))
* .onClick((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
*
* @method onClick
* @param {Function} handler Click handler function
* @returns {MenuItem} This instance for chaining
*/
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,
};
}
}