Skip to content

Commit fc38d6d

Browse files
Fix album delete-all stuck, add select-all, fix admin cleanup alignment
- Backend: Switch album delete-all from sequential blob deletion to batch soft-delete (instant) - Backend: Add SignalR broadcast after album delete for live UI updates - Frontend: Wrap album delete in runTask for progress tracking visibility - Frontend: Add Select All / Deselect All button in Gallery and Album Detail pages - Admin: Fix cleanup flyout table column alignment with fixed table layout - Admin: Pass correct totalCount from stats to flyout header - Admin: Bump soft-deleted API limit from 100 to 500, show overflow indicator
1 parent 986e7e1 commit fc38d6d

7 files changed

Lines changed: 66 additions & 20 deletions

File tree

client/src/pages/AdminPage.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,14 @@ function ActivityFeed() {
9595
);
9696
}
9797

98-
function CleanupFlyout({ onClose, onCleanup }: { onClose: () => void; onCleanup: () => Promise<void> }) {
98+
function CleanupFlyout({ onClose, onCleanup, totalCount }: { onClose: () => void; onCleanup: () => Promise<void>; totalCount?: number }) {
9999
const { data: items, isLoading } = useQuery({
100100
queryKey: ['admin-soft-deleted'],
101101
queryFn: adminApi.getSoftDeleted,
102102
});
103103

104+
const displayTotal = totalCount ?? items?.length ?? 0;
105+
104106
return (
105107
<div className="fixed inset-0 z-50 flex justify-end" style={{ background: 'rgba(0,0,0,0.5)' }} onClick={onClose}>
106108
<div
@@ -112,7 +114,7 @@ function CleanupFlyout({ onClose, onCleanup }: { onClose: () => void; onCleanup:
112114
<div className="flex items-center justify-between px-4 py-3 flex-shrink-0" style={{ borderBottom: '1px solid #30363d' }}>
113115
<h3 className="text-sm font-medium flex items-center gap-2" style={{ color: '#e6edf3' }}>
114116
<Trash2 size={16} style={{ color: '#fbbf24' }} />
115-
Pending Cleanup ({items?.length ?? 0} items)
117+
Pending Cleanup ({displayTotal} items)
116118
</h3>
117119
<div className="flex items-center gap-2">
118120
<button
@@ -135,21 +137,21 @@ function CleanupFlyout({ onClose, onCleanup }: { onClose: () => void; onCleanup:
135137
) : !items?.length ? (
136138
<div className="py-10 text-center" style={{ color: '#8b949e' }}>No soft-deleted items</div>
137139
) : (
138-
<table className="w-full">
140+
<table className="w-full" style={{ tableLayout: 'fixed' }}>
139141
<thead>
140142
<tr style={{ borderBottom: '1px solid #30363d', color: '#8b949e', position: 'sticky', top: 0, background: '#0d1117' }}>
141-
<th className="text-left px-3 py-2 font-normal">File</th>
142-
<th className="text-left px-3 py-2 font-normal">Type</th>
143-
<th className="text-left px-3 py-2 font-normal">Size</th>
144-
<th className="text-left px-3 py-2 font-normal">Status</th>
145-
<th className="text-left px-3 py-2 font-normal">Thumb</th>
146-
<th className="text-left px-3 py-2 font-normal">Deleted</th>
143+
<th className="text-left px-3 py-2 font-normal" style={{ width: '35%' }}>File</th>
144+
<th className="text-left px-3 py-2 font-normal" style={{ width: '10%' }}>Type</th>
145+
<th className="text-left px-3 py-2 font-normal" style={{ width: '10%' }}>Size</th>
146+
<th className="text-left px-3 py-2 font-normal" style={{ width: '12%' }}>Status</th>
147+
<th className="text-left px-3 py-2 font-normal" style={{ width: '8%' }}>Thumb</th>
148+
<th className="text-left px-3 py-2 font-normal" style={{ width: '25%' }}>Deleted</th>
147149
</tr>
148150
</thead>
149151
<tbody>
150152
{items.map((item) => (
151153
<tr key={item.id} style={{ borderBottom: '1px solid #21262d' }}>
152-
<td className="px-3 py-1.5 truncate max-w-[220px]" style={{ color: '#e6edf3' }}>{item.fileName}</td>
154+
<td className="px-3 py-1.5 truncate" style={{ color: '#e6edf3' }}>{item.fileName}</td>
153155
<td className="px-3 py-1.5" style={{ color: '#8b949e' }}>{item.contentType.split('/')[1] || item.contentType}</td>
154156
<td className="px-3 py-1.5" style={{ color: '#8b949e' }}>{item.sizeMB} MB</td>
155157
<td className="px-3 py-1.5">
@@ -165,6 +167,11 @@ function CleanupFlyout({ onClose, onCleanup }: { onClose: () => void; onCleanup:
165167
))}
166168
</tbody>
167169
</table>
170+
{items.length < displayTotal && (
171+
<div className="px-3 py-2 text-[10px] text-center" style={{ color: '#484f58', borderTop: '1px solid #21262d' }}>
172+
Showing {items.length} of {displayTotal} items
173+
</div>
174+
)}
168175
)}
169176
</div>
170177
</div>
@@ -432,7 +439,7 @@ export function AdminPage() {
432439
)}
433440

434441
{/* Soft-Deleted Items Flyout */}
435-
{showCleanupFlyout && <CleanupFlyout onClose={() => setShowCleanupFlyout(false)} onCleanup={handleTriggerCleanup} />}
442+
{showCleanupFlyout && <CleanupFlyout onClose={() => setShowCleanupFlyout(false)} onCleanup={handleTriggerCleanup} totalCount={stats?.softDeleted} />}
436443
</div>
437444
);
438445
}

client/src/pages/AlbumDetailPage.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,11 @@ export function AlbumDetailPage() {
199199
);
200200
if (!choice) return;
201201
const deleteMedia = choice.trim().toLowerCase() === 'all';
202-
await albumApi.deleteAlbum(albumId, deleteMedia);
203-
queryClient.invalidateQueries({ queryKey: ['albums'] });
202+
await runTask(`Deleting album "${data.album.name}"${deleteMedia ? ' and all media' : ''}`, async () => {
203+
await albumApi.deleteAlbum(albumId, deleteMedia);
204+
queryClient.invalidateQueries({ queryKey: ['albums'] });
205+
if (deleteMedia) queryClient.invalidateQueries({ queryKey: ['media'] });
206+
});
204207
navigate('/albums');
205208
};
206209

@@ -284,6 +287,20 @@ export function AlbumDetailPage() {
284287
>
285288
{selectMode ? <><Check size={14} className="inline mr-1" />{selectedIds.size} selected</> : 'Select'}
286289
</button>
290+
{selectMode && (
291+
<button
292+
onClick={() => {
293+
if (selectedIds.size === sortedMedia.length) {
294+
setSelectedIds(new Set());
295+
} else {
296+
setSelectedIds(new Set(sortedMedia.map(m => m.id)));
297+
}
298+
}}
299+
className="px-3 py-1.5 bg-zinc-800 text-zinc-400 hover:text-white rounded-md text-sm transition"
300+
>
301+
{selectedIds.size === sortedMedia.length ? 'Deselect All' : 'Select All'}
302+
</button>
303+
)}
287304
{selectMode && selectedIds.size > 0 && (
288305
<>
289306
<button onClick={handleDownloadSelected} className="px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 rounded-md text-sm text-white transition flex items-center gap-1">

client/src/pages/AlbumsPage.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import { useState } from 'react';
22
import { useNavigate } from 'react-router-dom';
33
import { useQuery, useQueryClient } from '@tanstack/react-query';
44
import { albumApi } from '../api/albumApi';
5+
import { useTrackedTask } from '../hooks/useTrackedTask';
56
import { FolderOpen, Trash2, EyeOff, Eye, Pencil, Lock } from 'lucide-react';
67

78
export function AlbumsPage() {
89
const navigate = useNavigate();
910
const queryClient = useQueryClient();
11+
const { runTask } = useTrackedTask();
1012
const { data: albums, isLoading } = useQuery({
1113
queryKey: ['albums'],
1214
queryFn: albumApi.getAll,
@@ -31,9 +33,11 @@ export function AlbumsPage() {
3133
);
3234
if (!choice) return;
3335
const deleteMedia = choice.trim().toLowerCase() === 'all';
34-
await albumApi.deleteAlbum(albumId, deleteMedia);
35-
queryClient.invalidateQueries({ queryKey: ['albums'] });
36-
if (deleteMedia) queryClient.invalidateQueries({ queryKey: ['media'] });
36+
await runTask(`Deleting album "${albumName}"${deleteMedia ? ' and all media' : ''}`, async () => {
37+
await albumApi.deleteAlbum(albumId, deleteMedia);
38+
queryClient.invalidateQueries({ queryKey: ['albums'] });
39+
if (deleteMedia) queryClient.invalidateQueries({ queryKey: ['media'] });
40+
});
3741
};
3842

3943
const handleToggleHidden = async (e: React.MouseEvent, album: { id: string; isHidden: boolean; isPasswordProtected: boolean }) => {

client/src/pages/GalleryPage.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,20 @@ export function GalleryPage() {
132132
>
133133
{selectMode ? <><Check size={14} className="inline mr-1" />{selectedIds.size} selected</> : 'Select'}
134134
</button>
135+
{selectMode && (
136+
<button
137+
onClick={() => {
138+
if (selectedIds.size === allItems.length) {
139+
setSelectedIds(new Set());
140+
} else {
141+
setSelectedIds(new Set(allItems.map(m => m.id)));
142+
}
143+
}}
144+
className="px-3 py-1.5 bg-zinc-800 text-zinc-400 hover:text-white rounded-md text-sm transition"
145+
>
146+
{selectedIds.size === allItems.length ? 'Deselect All' : 'Select All'}
147+
</button>
148+
)}
135149
{selectMode && selectedIds.size > 0 && (
136150
<>
137151
<button onClick={handleDownloadSelected} className="px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 rounded-md text-sm text-white transition flex items-center gap-1">

src/CleanSweep.API/Controllers/AdminController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ public async Task<ActionResult> GetSoftDeleted(CancellationToken ct)
409409
var items = await _db.MediaItems
410410
.Where(m => m.IsDeleted)
411411
.OrderByDescending(m => m.DeletedAt)
412-
.Take(100)
412+
.Take(500)
413413
.Select(m => new
414414
{
415415
m.Id,

src/CleanSweep.API/Controllers/AlbumController.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ public class AlbumController : ControllerBase
1616
private readonly AlbumService _albumService;
1717
private readonly IAlbumRepository _albumRepo;
1818
private readonly IBlobStorageService _blobService;
19+
private readonly INotificationService _notificationService;
1920
private readonly StorageOptions _storageOptions;
2021

21-
public AlbumController(AlbumService albumService, IAlbumRepository albumRepo, IBlobStorageService blobService, IOptions<StorageOptions> storageOptions)
22+
public AlbumController(AlbumService albumService, IAlbumRepository albumRepo, IBlobStorageService blobService, INotificationService notificationService, IOptions<StorageOptions> storageOptions)
2223
{
2324
_albumService = albumService;
2425
_albumRepo = albumRepo;
2526
_blobService = blobService;
27+
_notificationService = notificationService;
2628
_storageOptions = storageOptions.Value;
2729
}
2830

@@ -53,6 +55,7 @@ public async Task<ActionResult> RemoveMedia(Guid albumId, Guid mediaId, Cancella
5355
public async Task<ActionResult> DeleteAlbum(Guid albumId, [FromQuery] bool deleteMedia = false, CancellationToken ct = default)
5456
{
5557
await _albumService.DeleteAlbumAsync(albumId, deleteMedia, ct);
58+
await _notificationService.BroadcastMediaChangedAsync(ct);
5659
return NoContent();
5760
}
5861

src/CleanSweep.Application/Services/AlbumService.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,9 @@ public async Task DeleteAlbumAsync(Guid albumId, bool deleteMedia, CancellationT
9090
var album = await _albumRepo.GetByIdWithMediaAsync(albumId, ct);
9191
if (album != null)
9292
{
93-
foreach (var am in album.AlbumMedia)
94-
await _mediaService.DeleteMediaWithBlobsAsync(am.MediaId, ct);
93+
var mediaIds = album.AlbumMedia.Select(am => am.MediaId).ToList();
94+
if (mediaIds.Count > 0)
95+
await _mediaService.DeleteBatchAsync(mediaIds, ct);
9596
}
9697
}
9798
await _albumRepo.DeleteAsync(albumId, ct);

0 commit comments

Comments
 (0)