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:
Jordan Ramos
2026-06-03 13:55:10 -06:00
parent 1cc8bd5a4c
commit 4e8f4cbb10
2 changed files with 53 additions and 15 deletions

View File

@@ -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,
},

View File

@@ -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) =>
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}