diff --git a/docs/users-and-requests.md b/docs/users-and-requests.md index 7db97da7..3d4ae8f9 100644 --- a/docs/users-and-requests.md +++ b/docs/users-and-requests.md @@ -69,6 +69,10 @@ Admins can override the default ebook/audiobook modes and request rules for indi --- +## Clearing the activity list + +**Clear Completed** in the activity list clears finished downloads and their requests, and also finished requests that never had a download, such as a request closed because the book was already in the library. Pending and rejected requests are left alone. + ## Requests The request system controls whether users can download directly or need admin approval first. diff --git a/src/frontend/src/components/activity/ActivitySidebar.tsx b/src/frontend/src/components/activity/ActivitySidebar.tsx index 2c5ef884..847f46c1 100644 --- a/src/frontend/src/components/activity/ActivitySidebar.tsx +++ b/src/frontend/src/components/activity/ActivitySidebar.tsx @@ -379,17 +379,6 @@ export const ActivitySidebar = ({ }; }, [downloadItems, visibleRequestItems]); - const hasTerminalDownloadItems = useMemo( - () => - mergedDownloadItems.some( - (item) => - item.visualStatus === 'complete' || - item.visualStatus === 'error' || - item.visualStatus === 'cancelled', - ), - [mergedDownloadItems], - ); - const allItems = useMemo(() => { const combined = dedupeById([...mergedDownloadItems, ...mergedRequestItems]); return combined.toSorted((a, b) => b.timestamp - a.timestamp); @@ -958,8 +947,9 @@ export const ActivitySidebar = ({ })()} - {(effectiveActiveTab === 'downloads' || effectiveActiveTab === 'all') && - hasTerminalDownloadItems && + {(effectiveActiveTab === 'downloads' || + effectiveActiveTab === 'all' || + effectiveActiveTab === 'requests') && clearCompletedTargets.length > 0 && (
(); visibleItems.forEach((item) => { + if (item.kind === 'request' && typeof item.requestId === 'number') { + const record = item.requestRecord; + const finished = + record?.status === 'cancelled' || + (record?.status === 'fulfilled' && + !ACTIVE_DELIVERY_STATES.has(record.delivery_state ?? '')); + const requestKey = `request:${item.requestId}`; + if (finished && !pendingRequestIds.has(item.requestId) && !seen.has(requestKey)) { + seen.add(requestKey); + targets.push({ itemType: 'request', itemKey: requestKey }); + } + return; + } + const isTerminalDownload = item.kind === 'download' && (item.visualStatus === 'complete' || diff --git a/src/frontend/src/tests/clearCompleted.test.ts b/src/frontend/src/tests/clearCompleted.test.ts index 02e89e89..5d9c9f64 100644 --- a/src/frontend/src/tests/clearCompleted.test.ts +++ b/src/frontend/src/tests/clearCompleted.test.ts @@ -98,4 +98,32 @@ describe('getClearCompletedTargets', () => { expect(targets).toEqual([]); }); + + it('clears a finished request that never had a download', () => { + const closed = makeRequestItem(30, 'fulfilled'); + closed.requestRecord = Object.assign({}, closed.requestRecord, { + delivery_state: 'complete' as const, + admin_note: '[auto] Already in the library.', + }); + const cancelled = makeRequestItem(31, 'cancelled'); + + const targets = getClearCompletedTargets([closed, cancelled], [closed, cancelled]); + + expect(targets).toEqual([ + { itemType: 'request', itemKey: 'request:30' }, + { itemType: 'request', itemKey: 'request:31' }, + ]); + }); + + it('leaves pending, rejected and still-downloading requests alone', () => { + const pending = makeRequestItem(40, 'pending'); + const rejected = makeRequestItem(41, 'rejected'); + const downloading = makeRequestItem(42, 'fulfilled'); + downloading.requestRecord = Object.assign({}, downloading.requestRecord, { + delivery_state: 'downloading' as const, + }); + + const items = [pending, rejected, downloading]; + expect(getClearCompletedTargets(items, items)).toEqual([]); + }); });