feat(archives): Not Printed / Printed collections (#1153)

VP-uploaded archives land with status='archived' (uploaded but never
  sent to a printer), but the Archives page sidebar only offered
  All/Recent/This Week/This Month/Favorites/Failed/Duplicates -- no way
  to filter "what's still queued in my library" vs "what's been printed."

  Two new collections fill the gap: Not Printed filters to
  status==='archived'; Printed filters to any final-status archive
  (completed/failed/aborted/cancelled/stopped) so users see every
  archive that had a print attempt regardless of outcome (the existing
  Failed collection covers just the failure subset).

  Frontend-only -- the status field was already populated correctly by
  the VP archive paths, this was purely a UI gap. 2 new tests pin the
  filter behaviour against a 4-status fixture.
This commit is contained in:
maziggy
2026-04-29 07:02:45 +02:00
parent c2e7f8eb4b
commit b6a9d56651
5 changed files with 116 additions and 48 deletions
+1
View File
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
*/
import { describe, it, expect, beforeEach } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import { screen, waitFor, fireEvent } from '@testing-library/react';
import { render } from '../utils';
import { ArchivesPage } from '../../pages/ArchivesPage';
import { http, HttpResponse } from 'msw';
@@ -372,4 +372,59 @@ describe('ArchivesPage', () => {
// Upload Timelapse should also be disabled
});
});
// #1153 — Sylvain wanted to differentiate VP-uploaded archives (status='archived',
// never sent to a printer) from those that have been printed at least once.
describe('Not Printed / Printed collections', () => {
const mixedStatusArchives = [
{ ...mockArchives[0], id: 100, print_name: 'NeverPrinted', status: 'archived', started_at: null, completed_at: null },
{ ...mockArchives[0], id: 101, print_name: 'WasPrinted', status: 'completed' },
{ ...mockArchives[0], id: 102, print_name: 'WasFailed', status: 'failed' },
{ ...mockArchives[0], id: 103, print_name: 'WasCancelled', status: 'cancelled' },
];
beforeEach(() => {
// Reset persisted collection so each test starts on "All Archives".
window.localStorage.removeItem('archiveCollection');
server.use(
http.get('/api/v1/archives/', () => HttpResponse.json(mixedStatusArchives))
);
});
it('shows only archived (never-printed) entries when "Not Printed" is selected', async () => {
render(<ArchivesPage />);
await waitFor(() => {
expect(screen.getByText('NeverPrinted')).toBeInTheDocument();
});
const collectionSelect = screen.getByDisplayValue('All Archives');
fireEvent.change(collectionSelect, { target: { value: 'not-printed' } });
await waitFor(() => {
expect(screen.getByText('NeverPrinted')).toBeInTheDocument();
expect(screen.queryByText('WasPrinted')).not.toBeInTheDocument();
expect(screen.queryByText('WasFailed')).not.toBeInTheDocument();
expect(screen.queryByText('WasCancelled')).not.toBeInTheDocument();
});
});
it('shows only print-attempted entries (any final status) when "Printed" is selected', async () => {
render(<ArchivesPage />);
await waitFor(() => {
expect(screen.getByText('NeverPrinted')).toBeInTheDocument();
});
const collectionSelect = screen.getByDisplayValue('All Archives');
fireEvent.change(collectionSelect, { target: { value: 'printed' } });
await waitFor(() => {
expect(screen.queryByText('NeverPrinted')).not.toBeInTheDocument();
expect(screen.getByText('WasPrinted')).toBeInTheDocument();
expect(screen.getByText('WasFailed')).toBeInTheDocument();
expect(screen.getByText('WasCancelled')).toBeInTheDocument();
});
});
});
});
+13 -1
View File
@@ -2405,7 +2405,11 @@ function ArchiveListRow({
type SortOption = 'date-desc' | 'date-asc' | 'name-asc' | 'name-desc' | 'size-desc' | 'size-asc';
type ViewMode = 'grid' | 'list' | 'calendar' | 'log';
type Collection = 'all' | 'recent' | 'this-week' | 'this-month' | 'favorites' | 'failed' | 'duplicates';
type Collection = 'all' | 'recent' | 'this-week' | 'this-month' | 'favorites' | 'not-printed' | 'printed' | 'failed' | 'duplicates';
// status values that indicate a print attempt has finished (regardless of outcome).
// `archived` is the only status that means "uploaded but never sent to a printer."
const PRINTED_STATUSES = ['completed', 'failed', 'aborted', 'cancelled', 'stopped'] as const;
const collections: { id: Collection; label: string; icon: React.ReactNode }[] = [
{ id: 'all', label: 'All Archives', icon: <FolderOpen className="w-4 h-4" /> },
@@ -2413,6 +2417,8 @@ const collections: { id: Collection; label: string; icon: React.ReactNode }[] =
{ id: 'this-week', label: 'This Week', icon: <Calendar className="w-4 h-4" /> },
{ id: 'this-month', label: 'This Month', icon: <Calendar className="w-4 h-4" /> },
{ id: 'favorites', label: 'Favorites', icon: <Star className="w-4 h-4" /> },
{ id: 'not-printed', label: 'Not Printed', icon: <Upload className="w-4 h-4" /> },
{ id: 'printed', label: 'Printed', icon: <Printer className="w-4 h-4" /> },
{ id: 'failed', label: 'Failed Prints', icon: <AlertCircle className="w-4 h-4" /> },
{ id: 'duplicates', label: 'Duplicates', icon: <Copy className="w-4 h-4" /> },
];
@@ -2759,6 +2765,12 @@ export function ArchivesPage() {
case 'favorites':
matchesCollection = a.is_favorite === true;
break;
case 'not-printed':
matchesCollection = a.status === 'archived';
break;
case 'printed':
matchesCollection = (PRINTED_STATUSES as readonly string[]).includes(a.status);
break;
case 'failed':
matchesCollection = a.status === 'failed' || a.status === 'aborted';
break;
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -26,7 +26,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-5P0GaR3D.js"></script>
<script type="module" crossorigin src="/assets/index-CeqwraNQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-7GmlJb0k.css">
</head>
<body>