From e293aa4d2a6afb31c511669bb0aa1d10730342bf Mon Sep 17 00:00:00 2001 From: Flaminel Date: Mon, 19 Oct 2026 14:31:09 +0300 Subject: [PATCH] added content kind badge and filter to searches --- .../src/app/core/api/search-stats.api.ts | 4 ++- .../app/core/models/search-stats.models.ts | 8 ++++++ .../searches-tab/searches-tab.component.html | 12 ++++++++ .../searches-tab.component.spec.ts | 28 +++++++++++++++++-- .../searches-tab/searches-tab.component.ts | 13 ++++++++- 5 files changed, 61 insertions(+), 4 deletions(-) diff --git a/code/frontend/src/app/core/api/search-stats.api.ts b/code/frontend/src/app/core/api/search-stats.api.ts index 28d433ec..269f390c 100644 --- a/code/frontend/src/app/core/api/search-stats.api.ts +++ b/code/frontend/src/app/core/api/search-stats.api.ts @@ -2,7 +2,7 @@ import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import type { SearchStatsSummary, SearchEvent } from '@core/models/search-stats.models'; -import { SeekerSearchType, SeekerSearchReason, SearchCommandStatus } from '@core/models/search-stats.models'; +import { SeekerSearchType, SeekerSearchReason, SeekerContentKind, SearchCommandStatus } from '@core/models/search-stats.models'; import type { PaginatedResult } from '@core/models/pagination.model'; export enum SortDirection { @@ -28,6 +28,7 @@ export interface SearchEventsQuery { searchStatus?: SearchCommandStatus[]; searchType?: SeekerSearchType; searchReason?: SeekerSearchReason; + contentKind?: SeekerContentKind; grabbed?: boolean; } @@ -51,6 +52,7 @@ export class SearchStatsApi { if (query.sortDirection) params = params.set('sortDirection', query.sortDirection); if (query.searchType) params = params.set('searchType', query.searchType); if (query.searchReason) params = params.set('searchReason', query.searchReason); + if (query.contentKind) params = params.set('contentKind', query.contentKind); if (query.grabbed !== undefined) params = params.set('grabbed', String(query.grabbed)); if (query.searchStatus?.length) { diff --git a/code/frontend/src/app/core/models/search-stats.models.ts b/code/frontend/src/app/core/models/search-stats.models.ts index 89f86c53..29c65482 100644 --- a/code/frontend/src/app/core/models/search-stats.models.ts +++ b/code/frontend/src/app/core/models/search-stats.models.ts @@ -38,6 +38,13 @@ export enum SeekerSearchReason { Replacement = 'Replacement', } +export enum SeekerContentKind { + Standard = 'Standard', + Daily = 'Daily', + Anime = 'Anime', + Special = 'Special', +} + export enum SearchCommandStatus { Pending = 'Pending', Started = 'Started', @@ -59,4 +66,5 @@ export interface SearchEvent { grabbedItems: string[] | null; cycleId: string | null; isDryRun: boolean; + contentKind: SeekerContentKind | null; } diff --git a/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.html b/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.html index 33a6d91a..b1262fca 100644 --- a/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.html +++ b/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.html @@ -164,6 +164,9 @@ {{ formatSearchReason(event.searchReason) }} } + @if (event.contentKind) { + {{ event.contentKind }} + } @if (event.searchStatus) { {{ event.searchStatus }} @@ -269,6 +272,15 @@ /> +
+ Content kind + +
+
Grabbed = { grabbedItems: ['Release.One', 'Release.Two'], cycleId: 'cycle-aaaaaaaa-1111', isDryRun: false, + contentKind: SeekerContentKind.Special, }, { id: 'event-2', @@ -94,6 +95,7 @@ const EVENTS: PaginatedResult = { grabbedItems: null, cycleId: null, isDryRun: true, + contentKind: null, }, ], page: 1, @@ -198,6 +200,7 @@ describe('SearchesTabComponent', () => { searchStatus: undefined, searchType: undefined, searchReason: undefined, + contentKind: undefined, grabbed: undefined, }); }); @@ -213,24 +216,37 @@ describe('SearchesTabComponent', () => { component.toggleStatus(SearchCommandStatus.Completed); component.updateDraft('searchType', SeekerSearchType.Replacement); component.updateDraft('searchReason', SeekerSearchReason.Missing); + component.updateDraft('contentKind', SeekerContentKind.Anime); component.updateDraft('grabbed', 'false'); component.applyFilters(); fixture.detectChanges(); expect(component.drawerOpen()).toBe(false); expect(component.selectedInstanceId()).toBe('radarr-zulu'); - expect(component.activeFilterCount()).toBe(5); + expect(component.activeFilterCount()).toBe(6); expect(lastQuery()).toMatchObject({ instanceId: 'radarr-zulu', search: 'show', searchStatus: [SearchCommandStatus.Failed], searchType: SeekerSearchType.Replacement, searchReason: SeekerSearchReason.Missing, + contentKind: SeekerContentKind.Anime, grabbed: false, cycleId: undefined, }); }); + it('sends the chosen content kind in the events query', () => { + const { fixture, component, lastQuery } = setup(); + + component.openFilters(); + component.updateDraft('contentKind', SeekerContentKind.Special); + component.applyFilters(); + fixture.detectChanges(); + + expect(lastQuery().contentKind).toBe(SeekerContentKind.Special); + }); + it('resolves the current cycle id only for the selected instance and drops it when the instance is cleared', () => { const { fixture, component, lastQuery } = setup(); @@ -333,6 +349,14 @@ describe('SearchesTabComponent', () => { ); }); + it('shows a content kind badge only for an event that has one', () => { + const { fixture } = setup(); + + const rows = fixture.nativeElement.querySelectorAll('.list-row'); + expect((rows[0] as HTMLElement).textContent).toContain('Special'); + expect((rows[1] as HTMLElement).textContent).not.toContain('Special'); + }); + it('shows how many items remain in the cycle progress row', () => { const { fixture } = setup(); diff --git a/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.ts b/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.ts index 6ffa0614..764e5642 100644 --- a/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.ts +++ b/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.ts @@ -14,7 +14,7 @@ import { SearchStatsApi, SearchEventsSortBy, SortDirection } from '@core/api/sea import type { SearchEventsQuery } from '@core/api/search-stats.api'; import type { SearchStatsSummary, SearchEvent, InstanceSearchStat } from '@core/models/search-stats.models'; import type { PaginatedResult } from '@core/models/pagination.model'; -import { SeekerSearchType, SeekerSearchReason, SearchCommandStatus } from '@core/models/search-stats.models'; +import { SeekerSearchType, SeekerSearchReason, SeekerContentKind, SearchCommandStatus } from '@core/models/search-stats.models'; import { AppHubService } from '@core/realtime/app-hub.service'; import { ToastService } from '@core/services/toast.service'; import { PaginationService, PAGE_SIZE_STORAGE_KEYS } from '@core/services/pagination.service'; @@ -46,6 +46,7 @@ interface AdvancedFilters extends Record { statuses: SearchCommandStatus[]; searchType: SeekerSearchType | ''; searchReason: SeekerSearchReason | ''; + contentKind: SeekerContentKind | ''; grabbed: TriState; } @@ -55,6 +56,7 @@ const EMPTY_FILTERS: AdvancedFilters = { statuses: [], searchType: '', searchReason: '', + contentKind: '', grabbed: 'any', }; @@ -163,6 +165,7 @@ export class SearchesTabComponent { searchStatus: a.statuses.length ? a.statuses : undefined, searchType: a.searchType || undefined, searchReason: a.searchReason || undefined, + contentKind: a.contentKind || undefined, grabbed: triToBool(a.grabbed), }; }); @@ -207,6 +210,14 @@ export class SearchesTabComponent { { label: 'Replacement', value: SeekerSearchReason.Replacement }, ]; + readonly contentKindOptions: SelectOption[] = [ + { label: 'Any', value: '' }, + { label: 'Standard', value: SeekerContentKind.Standard }, + { label: 'Daily', value: SeekerContentKind.Daily }, + { label: 'Anime', value: SeekerContentKind.Anime }, + { label: 'Special', value: SeekerContentKind.Special }, + ]; + readonly triStateOptions: SelectOption[] = [ { label: 'Any', value: 'any' }, { label: 'Yes', value: 'true' },