fix(projects): drop deleted prints from their project, and refresh the view (#2731)

Deleting a print that belonged to a project left it on the project page as
a card with a missing thumbnail, and there was no way to remove it.

Deleting a print is a soft delete by default (#1343): the files go from
disk, the row stays so global Quick Stats keeps counting its filament,
time and cost. Every other consumer filters those rows out. The projects
module filtered none of them — the only deleted_at check in the whole file
was for LibraryFile — so a deleted print kept its project_id and kept
being listed, pointing at a thumbnail that no longer existed. The same
broken previews appeared on the overview cards, and in the timeline, where
the entry links to an archive that no longer opens. Unassigning was
impossible because the only UI that can change a print's project lives on
the Archives page, which correctly hides deleted prints: visible on the
project, unreachable from anywhere.

All eight project-scoped archive queries now filter, counts included. That
last part is a deliberate divergence from #1343, where the whole point of
the soft delete is that the contribution survives: a project is a piece of
work with a definite membership, not a lifetime total, so a project that
lists eleven prints must not claim twelve. The reasoning is recorded at
the constant so nobody later "fixes" it back.

remove_archives_from_project keeps working on hidden rows on purpose — it
is the repair path for links written before this. The BOM print_name
lookups are left alone; naming a since-deleted print is still correct.

Two more consumers had the same gap. The CSV/Excel export handed back rows
the interface says are gone — filtered at the base query, since the export
is the list you are looking at saved to a file. Per-project failure
analysis measured a failure rate against prints deleted from the project,
and disagreed with the project's own numbers; only the project-scoped
branch filters, global analysis still counts every run including orphans
as #1390 established.

Finally, the project page needed a manual reload to catch up. staleTime is
60s and the delete mutations invalidated only ['archives'], so a project
visited within the minute served its cached copy, print still there. The
project-assign mutations had the mirror-image bug: ['projects'] refreshed
the overview cards but never ['project', id]. Both now go through one
shared helper covering every project-derived key, as bare prefixes so all
cached project ids are matched.
This commit is contained in:
maziggy
2026-07-31 14:50:25 +02:00
parent 3abab1fd45
commit 284709f850
13 changed files with 556 additions and 116 deletions
@@ -6,6 +6,7 @@ import { api } from '../api/client';
import { Card, CardContent } from './Card';
import { Button } from './Button';
import { useToast } from '../contexts/ToastContext';
import { invalidateArchiveAndProjectViews } from '../utils/projectQueries';
interface BatchProjectModalProps {
selectedIds: number[];
@@ -43,14 +44,9 @@ export function BatchProjectModal({ selectedIds, onClose }: BatchProjectModalPro
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
// Helper to invalidate all project-related queries
const invalidateProjectQueries = () => {
queryClient.invalidateQueries({ queryKey: ['archives'] });
queryClient.invalidateQueries({ queryKey: ['projects'] });
// Invalidate project detail pages (partial match catches all project IDs)
queryClient.invalidateQueries({ queryKey: ['project'] });
queryClient.invalidateQueries({ queryKey: ['project-archives'] });
};
// Helper to invalidate all project-related queries. The shared version also
// covers the timeline and file-progress views, which this list was missing.
const invalidateProjectQueries = () => invalidateArchiveAndProjectViews(queryClient);
// Assign to project mutation (uses bulk API)
const assignMutation = useMutation({