From 6bf6371e51099891b20920b66d2dc0a9667dde90 Mon Sep 17 00:00:00 2001 From: jramos Date: Thu, 26 Mar 2026 14:52:06 -0600 Subject: [PATCH] feat(reporting): CARD workflow needs no vendor + own queue section CARD workflow type no longer requires a vendor/platform entry since asset disposition is handled entirely within CARD. In the popover the vendor field is replaced with a note when CARD is selected, and the Add button is enabled immediately. In the queue panel, CARD items are separated into their own top section (green header) rather than being mixed into vendor groups. Backend validation updated to skip vendor requirement for CARD. Co-Authored-By: Claude Sonnet 4.6 --- backend/routes/ivantiTodoQueue.js | 19 ++-- .../src/components/pages/ReportingPage.js | 90 ++++++++++++------- 2 files changed, 68 insertions(+), 41 deletions(-) diff --git a/backend/routes/ivantiTodoQueue.js b/backend/routes/ivantiTodoQueue.js index 6b83f18..e18395b 100644 --- a/backend/routes/ivantiTodoQueue.js +++ b/backend/routes/ivantiTodoQueue.js @@ -42,15 +42,20 @@ function createIvantiTodoQueueRouter(db, requireAuth) { if (!finding_id || typeof finding_id !== 'string' || finding_id.trim().length === 0) { return res.status(400).json({ error: 'finding_id is required.' }); } - if (!isValidVendor(vendor)) { - return res.status(400).json({ error: 'vendor is required (max 200 chars).' }); - } if (!VALID_WORKFLOW_TYPES.includes(workflow_type)) { - return res.status(400).json({ error: 'workflow_type must be FP or Archer.' }); + return res.status(400).json({ error: 'workflow_type must be FP, Archer, or CARD.' }); + } + // Vendor is required for FP and Archer, optional for CARD + if (workflow_type !== 'CARD' && !isValidVendor(vendor)) { + return res.status(400).json({ error: 'vendor is required for FP and Archer workflows.' }); + } + if (vendor !== undefined && vendor !== '' && !isValidVendor(vendor)) { + return res.status(400).json({ error: 'vendor must be under 200 chars.' }); } - const cvesJson = Array.isArray(cves) ? JSON.stringify(cves) : null; - const title = finding_title && typeof finding_title === 'string' + const vendorVal = workflow_type === 'CARD' ? '' : vendor.trim(); + const cvesJson = Array.isArray(cves) ? JSON.stringify(cves) : null; + const title = finding_title && typeof finding_title === 'string' ? finding_title.slice(0, 500) : null; @@ -58,7 +63,7 @@ function createIvantiTodoQueueRouter(db, requireAuth) { `INSERT INTO ivanti_todo_queue (user_id, finding_id, finding_title, cves_json, vendor, workflow_type) VALUES (?, ?, ?, ?, ?, ?)`, - [req.user.id, finding_id.trim(), title, cvesJson, vendor.trim(), workflow_type], + [req.user.id, finding_id.trim(), title, cvesJson, vendorVal, workflow_type], function (err) { if (err) { console.error('Error adding to queue:', err); diff --git a/frontend/src/components/pages/ReportingPage.js b/frontend/src/components/pages/ReportingPage.js index d1904ee..0b32740 100644 --- a/frontend/src/components/pages/ReportingPage.js +++ b/frontend/src/components/pages/ReportingPage.js @@ -1111,7 +1111,8 @@ function AddToQueuePopover({ finding, anchorRect, queueForm, setQueueForm, onAdd return () => document.removeEventListener('keydown', handler); }, [onCancel]); - const canSubmit = queueForm.vendor.trim().length > 0; + const isCard = queueForm.workflowType === 'CARD'; + const canSubmit = isCard || queueForm.vendor.trim().length > 0; return ReactDOM.createPortal(
- {/* Vendor input */} - + {/* Vendor input — hidden for CARD */} + {isCard ? ( +
+ No vendor required — disposition handled in CARD +
+ ) : ( + + )} {/* Workflow type toggle */}
@@ -1232,15 +1245,24 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onClearCompleted const pendingCount = items.filter((i) => i.status === 'pending').length; const completedCount = items.filter((i) => i.status === 'complete').length; - // Group by vendor, sorted alphabetically + // CARD items are their own top section; everything else groups by vendor const grouped = useMemo(() => { + const cardItems = items.filter((i) => i.workflow_type === 'CARD'); + const otherItems = items.filter((i) => i.workflow_type !== 'CARD'); + const map = {}; - items.forEach((item) => { + otherItems.forEach((item) => { const v = item.vendor || 'Unknown'; if (!map[v]) map[v] = []; map[v].push(item); }); - return Object.keys(map).sort().map((vendor) => ({ vendor, items: map[vendor] })); + const vendorGroups = Object.keys(map).sort().map((vendor) => ({ + key: vendor, label: vendor, items: map[vendor], isCard: false, + })); + + return cardItems.length > 0 + ? [{ key: '__CARD__', label: 'CARD', items: cardItems, isCard: true }, ...vendorGroups] + : vendorGroups; }, [items]); return ( @@ -1313,24 +1335,24 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onClearCompleted Check a row in the findings table to add it.
- ) : grouped.map(({ vendor, items: vendorItems }) => ( -
- {/* Vendor group header */} + ) : grouped.map(({ key, label, items: groupItems, isCard }) => ( +
+ {/* Group header */}
- - {vendor} + + {label} - {vendorItems.length} + {groupItems.length}
{/* Items */} - {vendorItems.map((item) => { + {groupItems.map((item) => { const done = item.status === 'complete'; const wfColor = item.workflow_type === 'FP' ? { col: '#F59E0B', rgb: '245,158,11' } : item.workflow_type === 'Archer' ? { col: '#0EA5E9', rgb: '14,165,233' }