Skip to content

Commit 6989ffa

Browse files
authored
Merge branch 'develop' into fix/select-all-books
2 parents 7296cba + 8dc3149 commit 6989ffa

32 files changed

Lines changed: 1116 additions & 1058 deletions

frontend/src/app/features/author-browser/components/author-detail/author-detail.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<ng-container *transloco="let t; prefix: 'authorBrowser.detail'">
2-
@if (loading) {
2+
@if (loading()) {
33
<div class="loading-state">
44
<p-progressSpinner strokeWidth="4" fill="transparent" animationDuration=".8s">
55
</p-progressSpinner>
66
<p style="color: var(--primary-color);">{{ t('loading') }}</p>
77
</div>
88
}
99

10-
@if (!loading && author(); as author) {
10+
@if (!loading() && author(); as author) {
1111
<div class="author-wrapper">
1212
<p-tabs [value]="tab" lazy="true" scrollable>
1313
<p-tablist>

frontend/src/app/features/author-browser/components/author-detail/author-detail.component.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ describe('AuthorDetailComponent', () => {
144144

145145
expect(getAuthorDetails).toHaveBeenCalledWith(42);
146146
expect(component.tab).toBe('series');
147-
expect(component.loading).toBe(false);
147+
expect(component.loading()).toBe(false);
148148
expect(component.author()).toEqual({...baseAuthor, id: 42, name: 'Grace Hopper'});
149149
expect(setPageTitle).toHaveBeenCalledWith('Grace Hopper');
150150
});
@@ -158,7 +158,7 @@ describe('AuthorDetailComponent', () => {
158158
component.ngOnInit();
159159

160160
expect(component.tab).toBe('books');
161-
expect(component.loading).toBe(false);
161+
expect(component.loading()).toBe(false);
162162
expect(component.author()).toBeNull();
163163
expect(setPageTitle).not.toHaveBeenCalled();
164164
});

frontend/src/app/features/author-browser/components/author-detail/author-detail.component.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export class AuthorDetailComponent implements OnInit, AfterViewChecked {
8585
private readonly viewportWidth = signal(0);
8686
private viewportResizeObserver: ResizeObserver | undefined;
8787

88-
loading = true;
88+
loading = signal(true);
8989
tab = 'books';
9090
isExpanded = false;
9191
isOverflowing = false;
@@ -199,11 +199,11 @@ export class AuthorDetailComponent implements OnInit, AfterViewChecked {
199199
this.authorService.getAuthorDetails(authorId).subscribe({
200200
next: (author) => {
201201
this.authorState.set(author);
202-
this.loading = false;
202+
this.loading.set(false);
203203
this.pageTitle.setPageTitle(author.name);
204204
},
205205
error: () => {
206-
this.loading = false;
206+
this.loading.set(false);
207207
}
208208
});
209209
}

frontend/src/app/features/book/components/book-browser/book-browser.component.html

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<div class="entity-info">
99
<p class="entity-title">
1010
@if (entityType === EntityType.ALL_BOOKS) {
11-
@if (currentFilterLabel() && rawFilterParamFromUrl) {
11+
@if (currentFilterLabel() && rawFilterParamFromUrl()) {
1212
{{ currentFilterLabel() }}
1313
} @else if (isFilterActive() || hasSearchTerm()) {
1414
{{ computedFilterLabel() }}
@@ -70,7 +70,7 @@
7070
<p-multiSelect
7171
optionLabel="header"
7272
[options]="columnPreferenceService.allColumns"
73-
[(ngModel)]="visibleColumns"
73+
[ngModel]="visibleColumns()"
7474
(ngModelChange)="onVisibleColumnsChange($event)"
7575
display="chip"
7676
[placeholder]="t('placeholder.selectColumns')"
@@ -85,14 +85,14 @@
8585
[label]="t('labels.save')"
8686
outlined
8787
size="small"
88-
(click)="columnPreferenceService.saveVisibleColumns(visibleColumns)"></p-button>
88+
(click)="columnPreferenceService.saveVisibleColumns(visibleColumns())"></p-button>
8989
</div>
9090
</p-popover>
9191
</div>
9292
}
9393
<a class="topbar-items topbar-item" role="button" tabindex="0" (click)="toggleTableGrid()" (keydown.enter)="toggleTableGrid()">
9494
<i
95-
[ngClass]="viewIcon()"
95+
[class]="viewIcon()"
9696
[pTooltip]="t('tooltip.toggleView')"
9797
tooltipPosition="top">
9898
</i>
@@ -180,14 +180,14 @@
180180
[pTooltip]="t('tooltip.selectSorting')"
181181
tooltipPosition="top">
182182
</i>
183-
@if (sortCriteriaCount > 1) {
184-
<p-badge [value]="sortCriteriaCount.toString()" class="sort-badge"/>
183+
@if (sortCriteriaCount() > 1) {
184+
<p-badge [value]="sortCriteriaCount().toString()" class="sort-badge"/>
185185
}
186186
</a>
187187
<p-popover #sortPopover [dismissable]="true" appendTo="body">
188188
<app-multi-sort-popover
189189
[sortCriteria]="this.bookSorter.selectedSortCriteria"
190-
[availableSortOptions]="visibleSortOptions"
190+
[availableSortOptions]="visibleSortOptions()"
191191
[showSaveButton]="canSaveSort()"
192192
(criteriaChange)="onSortCriteriaChange($event)"
193193
(saveSortConfig)="onSaveSortConfig($event)">
@@ -215,7 +215,7 @@
215215
type="text"
216216
pInputText
217217
[placeholder]="t('placeholder.search')"
218-
[(ngModel)]="bookTitle"
218+
[ngModel]="bookTitle()"
219219
(ngModelChange)="onSearchTermChange($event)"
220220
class="search-input-full"
221221
/>
@@ -237,11 +237,11 @@
237237
type="text"
238238
pInputText
239239
[placeholder]="t('placeholder.search')"
240-
[(ngModel)]="bookTitle"
240+
[ngModel]="bookTitle()"
241241
(ngModelChange)="onSearchTermChange($event)"
242242
class="search-input"
243243
/>
244-
@if (bookTitle) {
244+
@if (bookTitle()) {
245245
<p-button
246246
icon="pi pi-times"
247247
[text]="true"
@@ -314,7 +314,7 @@
314314
[preselectedBookIds]="selectedBooks()"
315315
(selectedBooksChange)="onSelectedBooksChange($event)"
316316
[sortOption]="this.bookSorter.selectedSort!"
317-
[visibleColumns]="visibleColumns"
317+
[visibleColumns]="visibleColumns()"
318318
></app-book-table>
319319
}
320320
@if (currentViewMode() === 'grid') {
@@ -330,7 +330,7 @@
330330
[index]="i"
331331
[book]="book"
332332
[isCheckboxEnabled]="true"
333-
[seriesViewEnabled]="seriesViewEnabled"
333+
[seriesViewEnabled]="seriesViewEnabled()"
334334
[onBookSelect]="onBookCardSelect"
335335
[isSeriesCollapsed]="seriesCollapseFilter.seriesCollapsed()"
336336
(checkboxClick)="onCheckboxClicked($event)"
@@ -358,7 +358,7 @@
358358
<div class="footer-center">
359359
@if (entityType(); as entityType) {
360360
<div class="action-buttons-group">
361-
@if (hasMetadataMenuItems) {
361+
@if (hasMetadataMenuItems()) {
362362
<p-tieredMenu #menu [model]="metadataMenuItems" [popup]="true" appendTo="body" styleClass="footer-menu"/>
363363
<p-button
364364
(click)="menu.toggle($event)"
@@ -418,7 +418,7 @@
418418
tooltipPosition="top">
419419
</p-button>
420420
}
421-
@if (hasMoreActionsItems) {
421+
@if (hasMoreActionsItems()) {
422422
<div class="more-actions-wrapper">
423423
<p-button
424424
(click)="menu.toggle($event)"

frontend/src/app/features/book/components/book-browser/book-browser.component.spec.ts

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {BookService} from '../../service/book.service';
1010
import {BookMetadataManageService} from '../../service/book-metadata-manage.service';
1111
import {LibraryShelfMenuService} from '../../service/library-shelf-menu.service';
1212
import {Book} from '../../model/book.model';
13-
import {SortDirection} from '../../model/sort.model';
13+
import {SortDirection, SortOption} from '../../model/sort.model';
1414
import {UserService} from '../../../settings/user-management/user.service';
1515
import {SeriesCollapseFilter} from './filters/SeriesCollapseFilter';
1616
import {CoverScalePreferenceService} from './cover-scale-preference.service';
@@ -112,6 +112,8 @@ interface BookBrowserHarness {
112112
updateFilters: ReturnType<typeof vi.fn>;
113113
updateFilterMode: ReturnType<typeof vi.fn>;
114114
updateMultiSort: ReturnType<typeof vi.fn>;
115+
parseQueryParams: ReturnType<typeof vi.fn>;
116+
syncQueryParams: ReturnType<typeof vi.fn>;
115117
};
116118
routeSnapshot: {
117119
routeConfig: {path: string};
@@ -129,8 +131,8 @@ function createHarness(options?: {
129131
}): BookBrowserHarness {
130132
const books = signal<Book[]>(
131133
options?.books ?? [
132-
makeBook(1, 1, 'Alpha', '2024-01-01T00:00:00Z'),
133134
makeBook(2, 1, 'Zulu', '2024-02-01T00:00:00Z'),
135+
makeBook(1, 1, 'Alpha', '2024-01-01T00:00:00Z'),
134136
makeBook(3, 2, 'Bravo', '2024-03-01T00:00:00Z'),
135137
]
136138
);
@@ -143,7 +145,17 @@ function createHarness(options?: {
143145
const paramMap$ = new BehaviorSubject(convertToParamMap({libraryId: '1'}));
144146
const queryParamMap$ = new BehaviorSubject(convertToParamMap({}));
145147
const url$ = new BehaviorSubject<unknown[]>([]);
148+
const defaultSort: SortOption = {field: 'addedOn', direction: SortDirection.DESCENDING, label: 'Added On'};
146149
const queryParamsService = {
150+
parseQueryParams: vi.fn((_q, _u, _et, _ei, _sortOptions, defaultFilterMode) => ({
151+
viewMode: VIEW_MODES.GRID,
152+
sortOption: defaultSort,
153+
sortCriteria: [defaultSort],
154+
filters: {},
155+
filterMode: defaultFilterMode,
156+
viewModeFromToggle: false,
157+
})),
158+
syncQueryParams: vi.fn(),
147159
shouldForceExpandSeries: vi.fn(() => false),
148160
updateViewMode: vi.fn(),
149161
updateFilters: vi.fn(),
@@ -157,6 +169,7 @@ function createHarness(options?: {
157169
params: {libraryId: '1'},
158170
};
159171

172+
TestBed.resetTestingModule();
160173
TestBed.configureTestingModule({
161174
providers: [
162175
SortService,
@@ -206,6 +219,35 @@ function createHarness(options?: {
206219
setCollapsed: vi.fn((value: boolean) => seriesCollapsed.set(value)),
207220
},
208221
},
222+
{
223+
provide: SortService,
224+
useValue: {
225+
applySort: vi.fn((b: Book[]) => b),
226+
},
227+
},
228+
{
229+
provide: BookSelectionService,
230+
useValue: {
231+
selectedBooks: signal([]),
232+
selectedCount: signal(0),
233+
deselectAll: vi.fn(),
234+
setCurrentBooks: vi.fn(),
235+
},
236+
},
237+
{
238+
provide: BookNavigationService,
239+
useValue: {
240+
setAvailableBookIds: vi.fn(),
241+
},
242+
},
243+
{
244+
provide: BookBrowserScrollService,
245+
useValue: {
246+
createKey: vi.fn(() => 'test-key'),
247+
savePosition: vi.fn(),
248+
getPosition: vi.fn(() => 0),
249+
},
250+
},
209251
{provide: ConfirmationService, useValue: {confirm: vi.fn()}},
210252
{provide: TaskHelperService, useValue: {}},
211253
{
@@ -255,12 +297,14 @@ function createHarness(options?: {
255297
if (f.libraryId) result = result.filter(b => b.libraryId === f.libraryId);
256298

257299
const s = _sort();
258-
const dir = s.dir === 'asc' ? 1 : -1;
259300
result = [...result].sort((a, b) => {
260-
if (s.field === 'title') return dir * ((a.metadata?.title ?? '') as string).localeCompare((b.metadata?.title ?? '') as string);
261-
if (s.field === 'addedOn') return dir * (a.addedOn ?? '').localeCompare(b.addedOn ?? '');
262-
return 0;
301+
const aVal = s.field === 'title' ? (a.metadata?.title ?? '') : (a.addedOn ?? '');
302+
const bVal = s.field === 'title' ? (b.metadata?.title ?? '') : (b.addedOn ?? '');
303+
return String(aVal).localeCompare(String(bVal));
263304
});
305+
if (s.dir === 'desc') {
306+
result.reverse();
307+
}
264308
return result;
265309
});
266310

@@ -384,7 +428,7 @@ describe('BookBrowserComponent', () => {
384428

385429
component.onViewModeChange(VIEW_MODES.TABLE);
386430

387-
expect(component.currentViewMode()).toBe(VIEW_MODES.TABLE);
431+
expect((component as unknown as {currentViewMode: () => string}).currentViewMode()).toBe(VIEW_MODES.TABLE);
388432
expect(queryParamsService.updateViewMode).toHaveBeenCalledWith(VIEW_MODES.TABLE);
389433
});
390434

@@ -404,7 +448,7 @@ describe('BookBrowserComponent', () => {
404448
TestBed.flushEffects();
405449

406450
expect(component.books()?.map(book => book.id)).toEqual([1, 2]);
407-
expect(component.hasRenderedBooks()).toBe(true);
451+
expect((component as unknown as {hasRenderedBooks: () => boolean}).hasRenderedBooks()).toBe(true);
408452
expect(component.isBooksRefreshing()).toBe(false);
409453
});
410454

@@ -420,12 +464,12 @@ describe('BookBrowserComponent', () => {
420464
TestBed.flushEffects();
421465

422466
expect(component.books()).toBeUndefined();
423-
expect(component.hasRenderedBooks()).toBe(false);
467+
expect((component as unknown as {hasRenderedBooks: () => boolean}).hasRenderedBooks()).toBe(false);
424468

425469
vi.runOnlyPendingTimers();
426470

427471
expect(component.books()?.map(book => book.id)).toEqual([3]);
428-
expect(component.hasRenderedBooks()).toBe(true);
472+
expect((component as unknown as {hasRenderedBooks: () => boolean}).hasRenderedBooks()).toBe(true);
429473
});
430474

431475
it('hides loading placeholders when the books query is in an error state', () => {
@@ -434,9 +478,25 @@ describe('BookBrowserComponent', () => {
434478
isBooksLoading: true,
435479
});
436480

437-
expect(component.showBooksLoadingPlaceholder()).toBe(false);
438-
expect(component.showGridLoadingPlaceholder()).toBe(false);
439-
expect(component.showTableLoadingPlaceholder()).toBe(false);
481+
const comp = component as unknown as {
482+
showBooksLoadingPlaceholder: () => boolean;
483+
showGridLoadingPlaceholder: () => boolean;
484+
showTableLoadingPlaceholder: () => boolean;
485+
};
486+
expect(comp.showBooksLoadingPlaceholder()).toBe(false);
487+
expect(comp.showGridLoadingPlaceholder()).toBe(false);
488+
expect(comp.showTableLoadingPlaceholder()).toBe(false);
489+
});
490+
491+
it('calls SeriesCollapseFilter.collapseBooks when computing pipelineInputs', () => {
492+
createHarness();
493+
const filter = TestBed.inject(SeriesCollapseFilter);
494+
const collapseBooksSpy = vi.spyOn(filter, 'collapseBooks');
495+
496+
vi.runOnlyPendingTimers();
497+
TestBed.flushEffects();
498+
499+
expect(collapseBooksSpy).toHaveBeenCalled();
440500
});
441501

442502
it('triggers next page fetch when scrolled near the bottom of rendered content', async () => {
@@ -462,7 +522,6 @@ describe('BookBrowserComponent', () => {
462522
removeEventListener: vi.fn(),
463523
} as unknown as HTMLElement;
464524

465-
component.currentViewMode.set(VIEW_MODES.GRID);
466525
component.scrollContainerRef = {nativeElement: mockElement} as ElementRef<HTMLElement>;
467526

468527
// Trigger initial check

0 commit comments

Comments
 (0)