10 KiB
Design Document: FP Submissions Cleanup
Overview
This feature addresses three usability improvements to the FP Submissions section in the Queue Panel:
- Auto-clear approved submissions — filter out approved FP submissions from the visible list so finalized items do not clutter the panel.
- Dismiss rejected submissions — add an X button on rejected submissions allowing users to permanently hide them from view, persisted in the database.
- Collapsible Submissions section — add a collapse/expand toggle on the SUBMISSIONS header, with state persisted in localStorage.
The design builds on the existing ivantiFpWorkflow.js route, the QueuePanel component's submissions section in ReportingPage.js, and the ivanti_fp_submissions table. Changes are minimal: one new database column (dismissed_at), one new API endpoint (PATCH /submissions/:id/dismiss), and frontend filtering/UI logic.
Architecture
sequenceDiagram
participant U as User
participant FE as React Frontend
participant BE as Express Backend
participant DB as PostgreSQL
Note over FE,DB: Auto-Clear Approved (frontend filter only)
FE->>BE: GET /api/ivanti/fp-workflow/submissions
BE->>DB: SELECT * FROM ivanti_fp_submissions WHERE user_id = $1
DB-->>BE: All submissions (including approved)
BE-->>FE: JSON array of submissions
FE->>FE: Filter out approved + dismissed submissions for display
Note over U,DB: Dismiss Rejected Submission
U->>FE: Click X button on rejected submission
FE->>BE: PATCH /api/ivanti/fp-workflow/submissions/:id/dismiss
BE->>DB: UPDATE ivanti_fp_submissions SET dismissed_at = NOW() WHERE id = $1
BE->>DB: INSERT audit_logs (action: ivanti_fp_submission_dismissed)
BE-->>FE: 200 OK
FE->>FE: Remove submission from visible list
Note over U,FE: Collapse/Expand Toggle
U->>FE: Click collapse toggle on SUBMISSIONS header
FE->>FE: Toggle collapsed state
FE->>FE: Persist to localStorage (key: steam_submissions_collapsed)
Components and Interfaces
Backend
Extended Route: backend/routes/ivantiFpWorkflow.js
One new endpoint added to the existing createIvantiFpWorkflowRouter.
Endpoint: PATCH /api/ivanti/fp-workflow/submissions/:id/dismiss
Marks a rejected submission as dismissed.
- Auth:
requireAuth(),requireGroup('Admin', 'Standard_User') - Ownership: verified via
user_idmatch (returns 403 otherwise) - Lifecycle guard: only submissions with
lifecycle_status === 'rejected'can be dismissed (returns 400 otherwise) - On success: sets
dismissed_at = NOW()on the submission record, logs audit entry - Response:
{ success: true }
Request: no body required.
Response:
{ "success": true }
Error responses:
- 404:
{ "error": "Submission not found." } - 403:
{ "error": "You can only dismiss your own submissions." } - 400:
{ "error": "Only rejected submissions can be dismissed." }
Modified Endpoint: GET /api/ivanti/fp-workflow/submissions
No changes to the backend query — it continues to return all submissions for the user. The frontend handles filtering. This preserves the ability to open approved submissions from workflow badges in read-only mode.
Pure Helper Function (exported for testing)
filterVisibleSubmissions(submissions)— new, filters an array of submission objects to exclude those withlifecycle_status === 'approved'ordismissed_at !== null. Returns the filtered array.
Frontend
QueuePanel Submissions Section Modifications
Filtering logic:
The fpSubmissions array passed to QueuePanel is filtered before rendering:
- Exclude submissions where
lifecycle_status === 'approved' - Exclude submissions where
dismissed_atis not null
This filtering is applied in the useMemo that already enriches fpSubmissionsRaw with Ivanti workflow state.
Dismiss button (X icon):
For submissions with lifecycle_status === 'rejected', render an X button (lucide X icon, 12px) on the right side of the submission row. On click:
- Call
PATCH /api/ivanti/fp-workflow/submissions/:id/dismiss - On success, re-fetch submissions (or optimistically remove from local state)
The X button uses e.stopPropagation() to prevent triggering the row's click-to-edit handler.
Collapse/expand toggle:
The SUBMISSIONS header row gets a clickable chevron icon:
- Expanded state:
ChevronDownicon (already imported from lucide-react) - Collapsed state:
ChevronUpicon
State management:
const [submissionsCollapsed, setSubmissionsCollapsed] = useState(() => localStorage.getItem('steam_submissions_collapsed') === 'true')- On toggle: flip state and persist to localStorage
- When collapsed: hide the submission list, keep the header visible with the count badge
- Default: expanded (when localStorage has no stored value)
localStorage key: steam_submissions_collapsed
FpEditModal — No Changes
The FpEditModal already handles approved submissions in read-only mode. Since the modal can be opened from workflow badges in the Reporting Table (which bypass the Queue Panel filter), no changes are needed.
Data Models
Schema Change to ivanti_fp_submissions
One new column:
ALTER TABLE ivanti_fp_submissions ADD COLUMN dismissed_at TIMESTAMPTZ DEFAULT NULL;
NULLmeans the submission is not dismissed (visible, subject to other filters)- A timestamp value means the submission was dismissed at that time (hidden from the Submissions section)
Migration Script: backend/migrations/add_fp_submissions_dismissed.js
Applies the schema change idempotently. Uses the same pattern as add_fp_submission_editing.js:
- PostgreSQL:
ALTER TABLE ... ADD COLUMN IF NOT EXISTS - Wrapped in try/catch for safety
const pool = require('../db');
async function run() {
console.log('Starting FP submissions dismissed migration...');
try {
await pool.query(`ALTER TABLE ivanti_fp_submissions ADD COLUMN IF NOT EXISTS dismissed_at TIMESTAMPTZ DEFAULT NULL`);
console.log('✓ dismissed_at column added (or already exists)');
} catch (err) {
console.error('Error adding dismissed_at column:', err.message);
process.exit(1);
}
console.log('Migration complete.');
process.exit(0);
}
run();
Correctness Properties
A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.
Property 1: Submission Visibility Filter
For any array of FP submission objects with arbitrary lifecycle_status values (from the set {submitted, approved, rejected, rework, resubmitted}) and arbitrary dismissed_at values (null or a timestamp string), filterVisibleSubmissions(submissions) should return only submissions where lifecycle_status is NOT "approved" AND dismissed_at is null. Additionally, every submission in the input that satisfies both conditions must appear in the output, and the output length must be less than or equal to the input length.
Validates: Requirements 1.1, 2.2, 2.3
Property 2: Dismiss Button Visibility Predicate
For any FP submission object with a lifecycle_status value drawn from {submitted, approved, rejected, rework, resubmitted} and a dismissed_at value (null or timestamp), the dismiss button should be rendered if and only if lifecycle_status === 'rejected' AND dismissed_at is null.
Validates: Requirements 2.1
Error Handling
Dismiss Endpoint Errors
| Condition | HTTP Status | Response | Behavior |
|---|---|---|---|
| Submission not found | 404 | { "error": "Submission not found." } |
No state change |
| User does not own submission | 403 | { "error": "You can only dismiss your own submissions." } |
No state change |
| Submission not in "rejected" status | 400 | { "error": "Only rejected submissions can be dismissed." } |
No state change |
| Database error | 500 | { "error": "Internal server error." } |
Log error, no state change |
Frontend Error Handling
- If the dismiss API call fails, show a brief error toast/message and leave the submission in the list
- If
localStorageis unavailable (private browsing), the collapse state defaults to expanded and is not persisted (graceful degradation)
Testing Strategy
Property-Based Testing
Use fast-check as the property-based testing library. Each correctness property maps to a single property-based test with a minimum of 100 iterations.
Property tests focus on the pure filtering function:
filterVisibleSubmissions(submissions)— Property 1- Dismiss button visibility predicate — Property 2
Tag format: Feature: fp-submissions-cleanup, Property {number}: {title}
Test file: backend/__tests__/fp-submissions-cleanup.property.test.js
Unit Testing
Unit tests cover specific examples, edge cases, and integration points:
- Filter with all approved: verify empty result when all submissions are approved
- Filter with all dismissed: verify empty result when all submissions are dismissed
- Filter with mixed statuses: verify correct subset returned
- Dismiss endpoint — happy path: verify
dismissed_atis set and audit log created - Dismiss endpoint — wrong status: verify 400 when attempting to dismiss a non-rejected submission
- Dismiss endpoint — ownership check: verify 403 when non-owner attempts dismiss
- Collapse state persistence: verify localStorage read/write with the correct key
- Collapse default: verify expanded state when localStorage has no value
Test file: backend/__tests__/fp-submissions-cleanup.test.js
Integration Testing
- Dismiss a rejected submission via API, re-fetch submissions, verify it's excluded from the response when filtered
- Verify audit log entry is created with correct action and entity_id
- Verify approved submissions are still accessible via direct ID lookup (for the edit modal)