Просмотр исходного кода

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.
maziggy 4 недель назад
Родитель
Сommit
b6a9d56651

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
CHANGELOG.md


+ 56 - 1
frontend/src/__tests__/pages/ArchivesPage.test.tsx

@@ -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
frontend/src/pages/ArchivesPage.tsx

@@ -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;

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-CeqwraNQ.js


+ 1 - 1
static/index.html

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

Некоторые файлы не были показаны из-за большого количества измененных файлов