From c38f4c6522cdc05d0bfb40889ffff97aa63ca845 Mon Sep 17 00:00:00 2001 From: splitsec2 <35583321+splitsec2@users.noreply.github.com> Date: Sun, 8 Nov 2026 15:52:19 -0600 Subject: [PATCH] feat(activity): Clear Completed also clears finished requests with no download (#1650) A request that auto-download closes because the book is already in the library never gets a download, so Clear Completed skipped it and those requests piled up under Complete. The button also disappeared once the last finished download was cleared, leaving no way to clear them. ActivitySidebar.tsx: hasTerminalDownloadItems is dropped; the button shows whenever getClearCompletedTargets returns anything, and on the Requests tab as well as Downloads and All. clearCompleted.ts: a request card with no download of its own is a target when its request is fulfilled with no active delivery_state (queued, resolving, locating, downloading), or cancelled. Pending and rejected requests are left alone. The server already treats any fulfilled request as terminal in _request_terminal_status, so the client rule is the stricter of the two. docs/users-and-requests.md: a line under Clearing the activity list saying what the button clears. Tests: two new cases in clearCompleted.test.ts, one for a closed request and a cancelled one with no download, one that pending, rejected and still-downloading requests are not touched. Builds on #1438. ## Summary by CodeRabbit * **New Features** * **Clear Completed** can appear on the Requests tab, as well as Downloads and All, when there are items to clear. * Clearing completed items removes finished downloads and their requests, fulfilled requests that are no longer in progress, and cancelled requests without downloads. Pending and rejected requests remain. * **Documentation** * Updated the help documentation to explain what **Clear Completed** removes. --- docs/users-and-requests.md | 4 +++ .../components/activity/ActivitySidebar.tsx | 16 ++--------- .../src/components/activity/clearCompleted.ts | 22 +++++++++++++++ src/frontend/src/tests/clearCompleted.test.ts | 28 +++++++++++++++++++ 4 files changed, 57 insertions(+), 13 deletions(-) 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([]); + }); });