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:
@@ -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,28 +1135,40 @@ function AddToQueuePopover({ finding, anchorRect, queueForm, setQueueForm, onAdd
|
||||
{finding.id}
|
||||
</div>
|
||||
|
||||
{/* Vendor input */}
|
||||
<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
|
||||
</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={queueForm.vendor}
|
||||
onChange={(e) => setQueueForm((f) => ({ ...f, vendor: e.target.value }))}
|
||||
placeholder="Juniper, Cisco, ADTRAN…"
|
||||
style={{
|
||||
width: '100%', boxSizing: 'border-box',
|
||||
background: 'rgba(14,165,233,0.05)',
|
||||
border: '1px solid rgba(14,165,233,0.2)',
|
||||
borderRadius: '0.25rem', padding: '0.35rem 0.5rem',
|
||||
color: '#CBD5E1', fontSize: '0.78rem',
|
||||
fontFamily: 'monospace', outline: 'none',
|
||||
}}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' && canSubmit) onAdd(); }}
|
||||
/>
|
||||
</label>
|
||||
{/* 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
|
||||
</span>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={queueForm.vendor}
|
||||
onChange={(e) => setQueueForm((f) => ({ ...f, vendor: e.target.value }))}
|
||||
placeholder="Juniper, Cisco, ADTRAN…"
|
||||
style={{
|
||||
width: '100%', boxSizing: 'border-box',
|
||||
background: 'rgba(14,165,233,0.05)',
|
||||
border: '1px solid rgba(14,165,233,0.2)',
|
||||
borderRadius: '0.25rem', padding: '0.35rem 0.5rem',
|
||||
color: '#CBD5E1', fontSize: '0.78rem',
|
||||
fontFamily: 'monospace', outline: 'none',
|
||||
}}
|
||||
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