fix(reporting): visible queue checkbox + multi-select delete
Table: removed disabled={queued} from the row checkbox so accentColor
renders properly — checked rows now show a solid blue tick instead of
the greyed-out browser default.
Queue panel: each item now has a small red selection checkbox (opacity
0.35 when idle, full when selected). Selecting any items reveals a red
'Delete (N)' button in the footer alongside 'Clear Completed'. Bulk
deletes run in parallel; selection state is automatically pruned when
items are removed via the individual trash button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1241,10 +1241,35 @@ function AddToQueuePopover({ finding, anchorRect, queueForm, setQueueForm, onAdd
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// QueuePanel — fixed slide-out panel showing the user's Ivanti queue
|
// QueuePanel — fixed slide-out panel showing the user's Ivanti queue
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function QueuePanel({ open, items, onClose, onUpdate, onDelete, onClearCompleted }) {
|
function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, onClearCompleted }) {
|
||||||
const pendingCount = items.filter((i) => i.status === 'pending').length;
|
const pendingCount = items.filter((i) => i.status === 'pending').length;
|
||||||
const completedCount = items.filter((i) => i.status === 'complete').length;
|
const completedCount = items.filter((i) => i.status === 'complete').length;
|
||||||
|
|
||||||
|
const [selectedIds, setSelectedIds] = useState(new Set());
|
||||||
|
|
||||||
|
// Drop any selected IDs that no longer exist in items
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedIds((prev) => {
|
||||||
|
if (prev.size === 0) return prev;
|
||||||
|
const valid = new Set(items.map((i) => i.id));
|
||||||
|
const next = new Set([...prev].filter((id) => valid.has(id)));
|
||||||
|
return next.size === prev.size ? prev : next;
|
||||||
|
});
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
const toggleSelect = (id) => {
|
||||||
|
setSelectedIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) next.delete(id); else next.add(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteSelected = () => {
|
||||||
|
onDeleteMany([...selectedIds]);
|
||||||
|
setSelectedIds(new Set());
|
||||||
|
};
|
||||||
|
|
||||||
// CARD items are their own top section; everything else groups by vendor
|
// CARD items are their own top section; everything else groups by vendor
|
||||||
const grouped = useMemo(() => {
|
const grouped = useMemo(() => {
|
||||||
const cardItems = items.filter((i) => i.workflow_type === 'CARD');
|
const cardItems = items.filter((i) => i.workflow_type === 'CARD');
|
||||||
@@ -1376,6 +1401,15 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onClearCompleted
|
|||||||
transition: 'opacity 0.15s',
|
transition: 'opacity 0.15s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Selection checkbox — for bulk delete */}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedIds.has(item.id)}
|
||||||
|
onChange={() => toggleSelect(item.id)}
|
||||||
|
style={{ accentColor: '#EF4444', width: '12px', height: '12px', flexShrink: 0, marginTop: '3px', cursor: 'pointer', opacity: selectedIds.has(item.id) ? 1 : 0.35 }}
|
||||||
|
title="Select for deletion"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Complete checkbox */}
|
{/* Complete checkbox */}
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -1455,12 +1489,32 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onClearCompleted
|
|||||||
padding: '0.75rem 1.25rem',
|
padding: '0.75rem 1.25rem',
|
||||||
borderTop: '1px solid rgba(255,255,255,0.06)',
|
borderTop: '1px solid rgba(255,255,255,0.06)',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
display: 'flex', gap: '0.5rem',
|
||||||
}}>
|
}}>
|
||||||
|
{/* Delete selected — only shown when items are selected */}
|
||||||
|
{selectedIds.size > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={handleDeleteSelected}
|
||||||
|
style={{
|
||||||
|
flex: 1, padding: '0.45rem',
|
||||||
|
background: 'rgba(239,68,68,0.1)',
|
||||||
|
border: '1px solid rgba(239,68,68,0.35)',
|
||||||
|
borderRadius: '0.375rem',
|
||||||
|
color: '#EF4444',
|
||||||
|
fontFamily: 'monospace', fontSize: '0.72rem', fontWeight: '600',
|
||||||
|
cursor: 'pointer',
|
||||||
|
textTransform: 'uppercase', letterSpacing: '0.05em',
|
||||||
|
transition: 'all 0.12s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete ({selectedIds.size})
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={onClearCompleted}
|
onClick={onClearCompleted}
|
||||||
disabled={completedCount === 0}
|
disabled={completedCount === 0}
|
||||||
style={{
|
style={{
|
||||||
width: '100%', padding: '0.45rem',
|
flex: 1, padding: '0.45rem',
|
||||||
background: completedCount > 0 ? 'rgba(16,185,129,0.08)' : 'transparent',
|
background: completedCount > 0 ? 'rgba(16,185,129,0.08)' : 'transparent',
|
||||||
border: `1px solid ${completedCount > 0 ? 'rgba(16,185,129,0.25)' : 'rgba(255,255,255,0.05)'}`,
|
border: `1px solid ${completedCount > 0 ? 'rgba(16,185,129,0.25)' : 'rgba(255,255,255,0.05)'}`,
|
||||||
borderRadius: '0.375rem',
|
borderRadius: '0.375rem',
|
||||||
@@ -1736,6 +1790,18 @@ export default function ReportingPage({ filterDate, filterEXC }) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const deleteQueueItems = useCallback(async (ids) => {
|
||||||
|
try {
|
||||||
|
await Promise.all(ids.map((id) =>
|
||||||
|
fetch(`${API_BASE}/ivanti/todo-queue/${id}`, { method: 'DELETE', credentials: 'include' })
|
||||||
|
));
|
||||||
|
const removed = new Set(ids);
|
||||||
|
setQueueItems((prev) => prev.filter((item) => !removed.has(item.id)));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error bulk-deleting queue items:', e);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const clearCompleted = useCallback(async () => {
|
const clearCompleted = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/ivanti/todo-queue/completed`, {
|
const res = await fetch(`${API_BASE}/ivanti/todo-queue/completed`, {
|
||||||
@@ -2201,7 +2267,6 @@ export default function ReportingPage({ filterDate, filterEXC }) {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
readOnly
|
readOnly
|
||||||
checked={queued}
|
checked={queued}
|
||||||
disabled={queued}
|
|
||||||
style={{
|
style={{
|
||||||
accentColor: '#0EA5E9',
|
accentColor: '#0EA5E9',
|
||||||
width: '13px', height: '13px',
|
width: '13px', height: '13px',
|
||||||
@@ -2263,6 +2328,7 @@ export default function ReportingPage({ filterDate, filterEXC }) {
|
|||||||
onClose={() => setQueueOpen(false)}
|
onClose={() => setQueueOpen(false)}
|
||||||
onUpdate={updateQueueItem}
|
onUpdate={updateQueueItem}
|
||||||
onDelete={deleteQueueItem}
|
onDelete={deleteQueueItem}
|
||||||
|
onDeleteMany={deleteQueueItems}
|
||||||
onClearCompleted={clearCompleted}
|
onClearCompleted={clearCompleted}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user