added content kind badge and filter to searches

This commit is contained in:
Flaminel committed 2026-10-20 15:55:54 +03:00
1 parent e934932b0f
commit e293aa4d2a
5 files changed
+61 -4

No files matched your search

@@ -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) {
@@ -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;
}
@@ -164,6 +164,9 @@
{{ formatSearchReason(event.searchReason) }}
</app-badge>
}
@if (event.contentKind) {
<app-badge severity="default" size="sm">{{ event.contentKind }}</app-badge>
}
@if (event.searchStatus) {
<app-badge [severity]="searchStatusSeverity(event.searchStatus)" size="sm">
{{ event.searchStatus }}
@@ -269,6 +272,15 @@
/>
</div>
<div class="filter-group">
<span class="filter-group__label">Content kind</span>
<app-select
[value]="draft().contentKind"
[options]="contentKindOptions"
(valueChange)="updateDraft('contentKind', $any($event))"
/>
</div>
<div class="filter-group">
<span class="filter-group__label">Grabbed</span>
<app-select
@@ -4,7 +4,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of, throwError } from 'rxjs';
import { SearchStatsApi, SearchEventsSortBy, SortDirection } from '@core/api/search-stats.api';
import type { SearchEventsQuery } from '@core/api/search-stats.api';
import { SeekerSearchType, SeekerSearchReason, SearchCommandStatus } from '@core/models/search-stats.models';
import { SeekerSearchType, SeekerSearchReason, SeekerContentKind, SearchCommandStatus } from '@core/models/search-stats.models';
import type { InstanceSearchStat, SearchEvent, SearchStatsSummary } from '@core/models/search-stats.models';
import type { PaginatedResult } from '@core/models/pagination.model';
import { AppHubService } from '@core/realtime/app-hub.service';
@@ -80,6 +80,7 @@ const EVENTS: PaginatedResult<SearchEvent> = {
grabbedItems: ['Release.One', 'Release.Two'],
cycleId: 'cycle-aaaaaaaa-1111',
isDryRun: false,
contentKind: SeekerContentKind.Special,
},
{
id: 'event-2',
@@ -94,6 +95,7 @@ const EVENTS: PaginatedResult<SearchEvent> = {
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();
@@ -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<string, unknown> {
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' },