Select multiple queue items and create a single consolidated Jira ticket with aggregated summary and description. Adds multi-select mode with checkboxes, floating action bar, consolidation modal, and junction table to track which queue items contributed to each ticket. - Migration: jira_ticket_queue_items junction table - POST /api/jira-tickets/:id/queue-items endpoint - GET /api/ivanti/todo-queue/ticket-links endpoint - ConsolidationModal component with aggregation logic - IvantiTodoQueuePage with selection mode and ticket link badges - Pure utility functions for summary/description generation - 34 tests passing (backend + frontend)
93 lines
2.9 KiB
JavaScript
93 lines
2.9 KiB
JavaScript
/**
|
|
* Pure utility functions for consolidating multiple Ivanti queue items
|
|
* into a single Jira ticket's summary, description, CVE, and vendor fields.
|
|
*/
|
|
|
|
/**
|
|
* Generate a consolidated summary for a multi-item Jira ticket.
|
|
* Format: "[N findings] vendor - first_finding_title", truncated to 255 chars.
|
|
*
|
|
* @param {Array} items - Array of queue item objects
|
|
* @returns {string} Generated summary, at most 255 characters
|
|
*/
|
|
export function generateConsolidatedSummary(items) {
|
|
const count = items.length;
|
|
const vendors = [...new Set(items.map(i => i.vendor).filter(Boolean))];
|
|
const vendorLabel = vendors.length === 1 ? vendors[0] : 'Multiple Vendors';
|
|
const firstTitle = items[0]?.finding_title || 'Untitled';
|
|
const raw = `[${count} findings] ${vendorLabel} - ${firstTitle}`;
|
|
return raw.slice(0, 255);
|
|
}
|
|
|
|
/**
|
|
* Generate a structured description grouped by vendor for a consolidated Jira ticket.
|
|
*
|
|
* @param {Array} items - Array of queue item objects
|
|
* @returns {string} Structured description with header and vendor-grouped items
|
|
*/
|
|
export function generateConsolidatedDescription(items) {
|
|
const header = `Consolidated Jira ticket covering ${items.length} Ivanti queue findings.\n\n`;
|
|
|
|
// Group by vendor
|
|
const grouped = {};
|
|
for (const item of items) {
|
|
const vendor = item.vendor || 'Unknown Vendor';
|
|
if (!grouped[vendor]) grouped[vendor] = [];
|
|
grouped[vendor].push(item);
|
|
}
|
|
|
|
let body = '';
|
|
for (const [vendor, vendorItems] of Object.entries(grouped)) {
|
|
body += `== ${vendor} ==\n`;
|
|
for (const item of vendorItems) {
|
|
let cves = 'None';
|
|
if (item.cves_json) {
|
|
try {
|
|
const parsed = JSON.parse(item.cves_json);
|
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
cves = parsed.join(', ');
|
|
}
|
|
} catch (e) {
|
|
cves = 'None';
|
|
}
|
|
}
|
|
body += `- ${item.finding_title}\n`;
|
|
body += ` CVEs: ${cves}\n`;
|
|
body += ` Host: ${item.hostname || 'N/A'} (${item.ip_address || 'N/A'})\n\n`;
|
|
}
|
|
}
|
|
|
|
return header + body;
|
|
}
|
|
|
|
/**
|
|
* Extract the first CVE from the first item that has a non-empty cves_json array.
|
|
*
|
|
* @param {Array} items - Array of queue item objects
|
|
* @returns {string} First CVE ID found, or empty string if none
|
|
*/
|
|
export function extractFirstCve(items) {
|
|
for (const item of items) {
|
|
if (item.cves_json) {
|
|
try {
|
|
const cves = JSON.parse(item.cves_json);
|
|
if (Array.isArray(cves) && cves.length > 0) return cves[0];
|
|
} catch (e) {
|
|
// Skip items with invalid JSON
|
|
}
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Extract the common vendor if all items share the same vendor.
|
|
*
|
|
* @param {Array} items - Array of queue item objects
|
|
* @returns {string} Common vendor name if all items share it, empty string otherwise
|
|
*/
|
|
export function extractCommonVendor(items) {
|
|
const vendors = [...new Set(items.map(i => i.vendor).filter(Boolean))];
|
|
return vendors.length === 1 ? vendors[0] : '';
|
|
}
|