/** * statusBadge — small helpers that build Node-RED status objects * ({ fill, shape, text }) consistently across every node. * * See CONTRACTS.md §7. Domains compose badges via these helpers so the * editor look-and-feel converges instead of every node rolling its own * emoji + colour rules. */ 'use strict'; const MAX_TEXT = 60; const SEPARATOR = ' | '; const DEFAULT_BADGE = { fill: 'green', shape: 'dot' }; const ERROR_BADGE = { fill: 'red', shape: 'ring' }; const IDLE_BADGE = { fill: 'blue', shape: 'dot' }; const UNKNOWN_BADGE = { fill: 'grey', shape: 'ring' }; // Truncate to MAX_TEXT keeping room for the ellipsis. Editor clips the // rest visually anyway, but we want the cut to be deterministic so // snapshot tests don't drift across Node-RED versions. function _clip(text) { if (text == null) return ''; const s = String(text); if (s.length <= MAX_TEXT) return s; return s.slice(0, MAX_TEXT - 1) + '…'; } function _joinParts(parts) { if (!Array.isArray(parts) || parts.length === 0) return ''; const kept = parts.filter((p) => p != null && p !== false && p !== ''); if (kept.length === 0) return ''; return kept.map(String).join(SEPARATOR); } function compose(parts, opts) { const text = _clip(_joinParts(parts)); return { fill: (opts && opts.fill) || DEFAULT_BADGE.fill, shape: (opts && opts.shape) || DEFAULT_BADGE.shape, text, }; } function error(message) { return { fill: ERROR_BADGE.fill, shape: ERROR_BADGE.shape, text: _clip(`⚠ ${message == null ? '' : message}`), }; } function idle(label) { return { fill: IDLE_BADGE.fill, shape: IDLE_BADGE.shape, text: _clip(`⏸️ ${label == null ? '' : label}`), }; } // Look up a state-template badge and optionally compose extra parts // into its text. Missing template falls back to a grey "unknown state" // badge — silent so caller can still surface the bad state through logs. function byState(stateMap, currentState, opts) { const template = stateMap && stateMap[currentState]; if (!template) { return { fill: UNKNOWN_BADGE.fill, shape: UNKNOWN_BADGE.shape, text: _clip(`unknown state: ${currentState == null ? '' : currentState}`), }; } const baseText = template.text == null ? '' : String(template.text); const extras = opts && Array.isArray(opts.compose) ? opts.compose : []; const merged = extras.length > 0 ? _joinParts([baseText, ...extras]) : baseText; return { fill: template.fill || DEFAULT_BADGE.fill, shape: template.shape || DEFAULT_BADGE.shape, text: _clip(merged), }; } function text(string, opts) { return { fill: (opts && opts.fill) || DEFAULT_BADGE.fill, shape: (opts && opts.shape) || DEFAULT_BADGE.shape, text: _clip(string == null ? '' : string), }; } const statusBadge = { compose, error, idle, byState, text }; module.exports = { statusBadge, MAX_TEXT };