Allow redirecting pending queue items in place without duplicating
Previously, redirecting a queue item required completing it first, which created a duplicate entry. Now: - Pending items: redirect updates workflow_type in place (no new row) - Completed items: still creates a new pending item (legacy behavior) - Redirect arrow now visible on all items, not just completed ones - Frontend handles in-place updates by replacing the item in state
This commit is contained in:
@@ -318,16 +318,17 @@ function createIvantiTodoQueueRouter() {
|
||||
/**
|
||||
* POST /api/ivanti/todo-queue/:id/redirect
|
||||
*
|
||||
* Redirects a completed queue item to a different workflow by creating a new
|
||||
* pending queue item with the same finding data but a new workflow type/vendor.
|
||||
* Redirects a queue item to a different workflow type. If the item is pending,
|
||||
* updates workflow_type in place. If the item is complete, creates a new pending
|
||||
* queue item with the same finding data but a new workflow type/vendor.
|
||||
* Requires Admin or Standard_User group.
|
||||
*
|
||||
* @param {string} id — Queue item ID of the completed item (URL parameter)
|
||||
* @param {string} id — Queue item ID (URL parameter)
|
||||
* @body {Object}
|
||||
* - workflow_type {string} Required. One of: FP, Archer, CARD, GRANITE, DECOM
|
||||
* - vendor {string} Required for FP, Archer, and DECOM workflows; max 200 chars
|
||||
* @returns {Object} The newly created queue item with parsed `cves` array
|
||||
* @error 400 Invalid input or item not in complete status
|
||||
* @returns {Object} The updated or newly created queue item with parsed `cves` array
|
||||
* @error 400 Invalid input
|
||||
* @error 404 Queue item not found
|
||||
* @error 500 Internal server error
|
||||
*/
|
||||
@@ -358,10 +359,38 @@ function createIvantiTodoQueueRouter() {
|
||||
if (!original) {
|
||||
return res.status(404).json({ error: 'Queue item not found.' });
|
||||
}
|
||||
if (original.status !== 'complete') {
|
||||
return res.status(400).json({ error: 'Only completed queue items can be redirected.' });
|
||||
|
||||
// If the item is still pending, update workflow_type in place (no duplication)
|
||||
if (original.status === 'pending') {
|
||||
const { rows } = await pool.query(
|
||||
`UPDATE ivanti_todo_queue SET workflow_type = $1, vendor = $2, updated_at = NOW()
|
||||
WHERE id = $3 AND user_id = $4 RETURNING *`,
|
||||
[workflow_type, vendorVal, id, req.user.id]
|
||||
);
|
||||
|
||||
logAudit({
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'queue_item_redirected',
|
||||
entityType: 'ivanti_todo_queue',
|
||||
entityId: String(original.id),
|
||||
details: {
|
||||
original_workflow_type: original.workflow_type,
|
||||
target_workflow_type: workflow_type,
|
||||
method: 'in_place_update',
|
||||
vendor: vendorVal,
|
||||
},
|
||||
ipAddress: req.ip,
|
||||
});
|
||||
|
||||
const result = {
|
||||
...rows[0],
|
||||
cves: rows[0].cves_json ? JSON.parse(rows[0].cves_json) : [],
|
||||
};
|
||||
return res.json(result);
|
||||
}
|
||||
|
||||
// If the item is complete, create a new pending item (legacy behavior)
|
||||
const { rows } = await pool.query(
|
||||
`INSERT INTO ivanti_todo_queue
|
||||
(user_id, finding_id, finding_title, cves_json, ip_address, hostname, vendor, workflow_type)
|
||||
@@ -379,6 +408,7 @@ function createIvantiTodoQueueRouter() {
|
||||
details: {
|
||||
original_workflow_type: original.workflow_type,
|
||||
target_workflow_type: workflow_type,
|
||||
method: 'new_item_from_complete',
|
||||
new_item_id: rows[0].id,
|
||||
vendor: vendorVal,
|
||||
},
|
||||
|
||||
@@ -2002,13 +2002,13 @@ function QueuePanel({ open, items, onClose, onUpdate, onDelete, onDeleteMany, on
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Redirect button — completed items only */}
|
||||
{canWrite && done && (
|
||||
{/* Redirect button — available on all items */}
|
||||
{canWrite && (
|
||||
<button
|
||||
onClick={() => setRedirectItem(item)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#334155', padding: '1px', lineHeight: 1, flexShrink: 0 }}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: done ? '#334155' : '#475569', padding: '1px', lineHeight: 1, flexShrink: 0 }}
|
||||
onMouseEnter={(e) => e.currentTarget.style.color = '#0EA5E9'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.color = '#334155'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.color = done ? '#334155' : '#475569'}
|
||||
title="Redirect to another workflow"
|
||||
>
|
||||
<CornerUpRight style={{ width: '13px', height: '13px' }} />
|
||||
@@ -7273,10 +7273,18 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) {
|
||||
onDeleteMany={deleteQueueItems}
|
||||
onClearCompleted={clearCompleted}
|
||||
onCreateFpWorkflow={handleCreateFpWorkflow}
|
||||
onRedirectComplete={(newItem) => {
|
||||
setQueueItems((prev) => [...prev, newItem].sort((a, b) =>
|
||||
(a.vendor || '').localeCompare(b.vendor || '') || a.id - b.id
|
||||
));
|
||||
onRedirectComplete={(updatedItem) => {
|
||||
setQueueItems((prev) => {
|
||||
// If item already exists (in-place update), replace it
|
||||
const exists = prev.some(i => i.id === updatedItem.id);
|
||||
if (exists) {
|
||||
return prev.map(i => i.id === updatedItem.id ? updatedItem : i);
|
||||
}
|
||||
// Otherwise it's a new item (redirect from completed), add it
|
||||
return [...prev, updatedItem].sort((a, b) =>
|
||||
(a.vendor || '').localeCompare(b.vendor || '') || a.id - b.id
|
||||
);
|
||||
});
|
||||
}}
|
||||
canWrite={canWrite}
|
||||
fpSubmissions={fpSubmissionsFiltered}
|
||||
|
||||
Reference in New Issue
Block a user