// // Node model shared by every sidebar section. Nova's TreeItem has no badge or // status colour, so run state is carried by a leading glyph in the item name // and by descriptive text on the trailing edge. // const time = require("../util/time.js"); const RUN_GLYPHS = { success: "✓", failure: "✗", failed: "✗", error: "✗", timed_out: "✗", cancelled: "⊘", canceled: "⊘", skipped: "⊝", running: "●", in_progress: "●", queued: "◌", waiting: "◌", pending: "◌", blocked: "◌", }; function glyphFor(statusOrConclusion) { const key = String(statusOrConclusion || "").toLowerCase(); return RUN_GLYPHS[key] || "•"; } /** A completed run reports state via `conclusion`; a live one via `status`. */ function runState(run) { const conclusion = String(run.conclusion || "").toLowerCase(); if (conclusion) return conclusion; return String(run.status || "").toLowerCase() || "unknown"; } function pullRequestGlyph(pull) { if (pull.merged) return "⤳"; if (pull.state === "closed") return "⊘"; if (pull.draft) return "◌"; return "◆"; } function reviewGlyph(state) { const key = String(state || "").toUpperCase(); if (key === "APPROVED") return "✓"; if (key === "REQUEST_CHANGES" || key === "CHANGES_REQUESTED") return "✗"; if (key === "PENDING") return "◌"; return "◆"; } let counter = 0; /** * Builds a tree node. `children` is a resolved array; `load` is an async * producer used for rows that fetch on expand. */ function node(kind, name, options = {}) { counter += 1; const value = { kind: kind, name: name, identifier: options.identifier || `${kind}-${counter}`, descriptiveText: options.description || "", tooltip: options.tooltip || "", image: options.image || null, contextValue: options.contextValue || kind, command: options.command || null, path: options.path || null, data: options.data || null, repo: options.repo || null, children: options.children || null, load: options.load || null, expanded: options.expanded === true, parent: null, }; if (value.children) { for (const child of value.children) child.parent = value; } return value; } function message(text, kind = "message") { return node(kind, text, { contextValue: "message" }); } function errorNode(text) { return node("error", `⚠ ${text}`, { contextValue: "message", tooltip: text }); } /** Formats "12s · 3m ago" style trailing text for a run or job. */ function timingText(entity) { const parts = []; const length = time.duration(entity.started_at, entity.completed_at); if (length) parts.push(length); const when = time.relative(entity.started_at || entity.created_at || entity.updated_at); if (when) parts.push(when); return parts.join(" · "); } function runTitle(run) { return ( run.display_title || run.title || run.name || (run.path ? run.path.split("/").pop() : null) || `Run #${run.run_number || run.id}` ); } /** Groups runs by their workflow file, falling back to the run's own name. */ function workflowKey(run) { if (run.path) return run.path; if (run.workflow_id) return String(run.workflow_id); return run.name || "Workflow"; } function workflowLabel(run) { if (run.path) return run.path.split("/").pop(); return run.name || "Workflow"; } exports.node = node; exports.message = message; exports.errorNode = errorNode; exports.glyphFor = glyphFor; exports.runState = runState; exports.pullRequestGlyph = pullRequestGlyph; exports.reviewGlyph = reviewGlyph; exports.timingText = timingText; exports.runTitle = runTitle; exports.workflowKey = workflowKey; exports.workflowLabel = workflowLabel;