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 <noreply@anthropic.com>
This commit is contained in:
@@ -42,13 +42,18 @@ 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 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)
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
<div
|
||||
@@ -1134,7 +1135,18 @@ function AddToQueuePopover({ finding, anchorRect, queueForm, setQueueForm, onAdd
|
||||
{finding.id}
|
||||
</div>
|
||||
|
||||
{/* Vendor input */}
|
||||
{/* Vendor input — hidden for CARD */}
|
||||
{isCard ? (
|
||||
<div style={{
|
||||
marginBottom: '0.625rem', padding: '0.4rem 0.5rem',
|
||||
background: 'rgba(16,185,129,0.06)',
|
||||
border: '1px solid rgba(16,185,129,0.2)',
|
||||
borderRadius: '0.25rem',
|
||||
fontFamily: 'monospace', fontSize: '0.68rem', color: '#10B981',
|
||||
}}>
|
||||
No vendor required — disposition handled in CARD
|
||||
</div>
|
||||
) : (
|
||||
<label style={{ display: 'block', marginBottom: '0.625rem' }}>
|
||||
<span style={{ display: 'block', fontFamily: 'monospace', fontSize: '0.62rem', fontWeight: '600', color: '#64748B', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: '0.3rem' }}>
|
||||
Vendor / Platform
|
||||
@@ -1156,6 +1168,7 @@ function AddToQueuePopover({ finding, anchorRect, queueForm, setQueueForm, onAdd
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' && canSubmit) onAdd(); }}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/* Workflow type toggle */}
|
||||
<div style={{ marginBottom: '0.875rem' }}>
|
||||
@@ -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.
|
||||
</span>
|
||||
</div>
|
||||
) : grouped.map(({ vendor, items: vendorItems }) => (
|
||||
<div key={vendor} style={{ marginBottom: '1.25rem' }}>
|
||||
{/* Vendor group header */}
|
||||
) : grouped.map(({ key, label, items: groupItems, isCard }) => (
|
||||
<div key={key} style={{ marginBottom: '1.25rem' }}>
|
||||
{/* Group header */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '0.3rem 0', marginBottom: '0.375rem',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.06)',
|
||||
borderBottom: `1px solid ${isCard ? 'rgba(16,185,129,0.2)' : 'rgba(255,255,255,0.06)'}`,
|
||||
}}>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '700', color: '#94A3B8', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
||||
{vendor}
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '0.68rem', fontWeight: '700', color: isCard ? '#10B981' : '#94A3B8', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
||||
{label}
|
||||
</span>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '0.62rem', color: '#334155' }}>
|
||||
{vendorItems.length}
|
||||
{groupItems.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 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' }
|
||||
|
||||
Reference in New Issue
Block a user