Skip to content

Commit 591e3ea

Browse files
committed
refactor(ui): use paginated author query for metadata autocomplete
1 parent 7195c87 commit 591e3ea

13 files changed

Lines changed: 117 additions & 49 deletions
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {computed, inject, Injectable, signal} from '@angular/core';
2+
import type {ScrollerOptions} from '@openng/optimus-ui/api';
3+
import type {ScrollerScrollEvent} from '@openng/optimus-ui/types/scroller';
4+
import {injectInfiniteQuery} from '@tanstack/angular-query-experimental';
5+
6+
import {AuthorQueryService} from './author-query.service';
7+
import {DEFAULT_AUTHOR_SORT_TERMS, EMPTY_AUTHOR_FACET_SELECTION} from './author-query-params';
8+
9+
const PAGE_SIZE = 20;
10+
11+
@Injectable()
12+
export class AuthorAutocompleteService {
13+
private readonly authorQueryService = inject(AuthorQueryService);
14+
private readonly searchTerm = signal('');
15+
private readonly query = injectInfiniteQuery(() => {
16+
const query = this.searchTerm();
17+
return {
18+
...this.authorQueryService.infinitePage({
19+
query,
20+
facets: EMPTY_AUTHOR_FACET_SELECTION,
21+
facetLogic: 'and',
22+
sort: DEFAULT_AUTHOR_SORT_TERMS,
23+
size: PAGE_SIZE,
24+
}),
25+
enabled: query.length > 0,
26+
};
27+
});
28+
29+
readonly suggestions = computed(() => {
30+
if (this.searchTerm().length === 0 || this.query.isError()) return [];
31+
return this.query.data()?.pages.flatMap(page => page.content.map(author => author.name)) ?? [];
32+
});
33+
readonly virtualScrollOptions: ScrollerOptions = {
34+
onScroll: (event: ScrollerScrollEvent) => this.loadMore(event),
35+
};
36+
37+
search(query: string): void {
38+
this.searchTerm.set(query.trim());
39+
}
40+
41+
reset(): void {
42+
this.searchTerm.set('');
43+
}
44+
45+
private loadMore(event: ScrollerScrollEvent): void {
46+
const element = event.originalEvent?.target as HTMLElement | null;
47+
if (!element || element.scrollTop + element.clientHeight < element.scrollHeight - 1) return;
48+
if (!this.query.hasNextPage() || this.query.isFetchingNextPage()) return;
49+
void this.query.fetchNextPage();
50+
}
51+
}

frontend/src/app/features/book/components/add-physical-book-dialog/add-physical-book-dialog.component.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,14 @@ <h2 class="panel-title">{{ t('title') }}</h2>
103103
[multiple]="true"
104104
[fluid]="true"
105105
[dropdown]="false"
106-
[suggestions]="filteredAuthors"
106+
[suggestions]="authorAutocomplete.suggestions()"
107107
[forceSelection]="false"
108108
[showClear]="false"
109+
[virtualScroll]="true"
110+
[virtualScrollItemSize]="40"
111+
[virtualScrollOptions]="authorAutocomplete.virtualScrollOptions"
109112
[placeholder]="t('authorsPlaceholder')"
110-
(completeMethod)="filterAuthors($event)"
113+
(completeMethod)="authorAutocomplete.search($event.query)"
111114
(onKeyUp)="onAutoCompleteKeyUp('authors', $event)"
112115
(onSelect)="onAutoCompleteSelect('authors', $event)">
113116
</p-autoComplete>

frontend/src/app/features/book/components/add-physical-book-dialog/add-physical-book-dialog.component.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {Library} from '../../model/library.model';
1010
import {BookMetadataService} from '../../service/book-metadata.service';
1111
import {BookService} from '../../service/book.service';
1212
import {LibraryService} from '../../service/library.service';
13+
import {AuthorAutocompleteService} from '../../../author-browser/data/author-autocomplete.service';
1314
import {AddPhysicalBookDialogComponent} from './add-physical-book-dialog.component';
1415

1516
describe('AddPhysicalBookDialogComponent', () => {
@@ -83,6 +84,7 @@ describe('AddPhysicalBookDialogComponent', () => {
8384
{provide: BookService, useValue: {uniqueMetadata, createPhysicalBook}},
8485
{provide: BookMetadataService, useValue: {lookupByIsbn}},
8586
{provide: LibraryService, useValue: {libraries}},
87+
{provide: AuthorAutocompleteService, useValue: {reset: vi.fn()}},
8688
],
8789
});
8890

@@ -141,13 +143,11 @@ describe('AddPhysicalBookDialogComponent', () => {
141143
expect(component.selectedLibraryId).toBe(1);
142144
});
143145

144-
it('filters authors and categories with case-insensitive substring matches', () => {
146+
it('filters categories with case-insensitive substring matches', () => {
145147
const {component} = createHarness();
146148

147-
component.filterAuthors({query: 'taV', originalEvent: new Event('input')} as AutoCompleteCompleteEvent);
148149
component.filterCategories({query: 'fic', originalEvent: new Event('input')} as AutoCompleteCompleteEvent);
149150

150-
expect(component.filteredAuthors).toEqual(['Octavia Butler']);
151151
expect(component.filteredCategories).toEqual(['Science Fiction']);
152152
});
153153

frontend/src/app/features/book/components/add-physical-book-dialog/add-physical-book-dialog.component.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {LibraryService} from '../../service/library.service';
1414
import {Library} from '../../model/library.model';
1515
import {CreatePhysicalBookRequest} from '../../model/book.model';
1616
import {TranslocoDirective} from '@jsverse/transloco';
17+
import {AuthorAutocompleteService} from '../../../author-browser/data/author-autocomplete.service';
1718

1819
@Component({
1920
selector: 'app-add-physical-book-dialog',
@@ -31,13 +32,15 @@ import {TranslocoDirective} from '@jsverse/transloco';
3132
TranslocoDirective
3233
],
3334
styleUrl: './add-physical-book-dialog.component.scss',
35+
providers: [AuthorAutocompleteService],
3436
})
3537
export class AddPhysicalBookDialogComponent {
3638
private dynamicDialogRef = inject(DynamicDialogRef);
3739
private dialogConfig = inject(DynamicDialogConfig);
3840
private bookService = inject(BookService);
3941
private bookMetadataService = inject(BookMetadataService);
4042
private libraryService = inject(LibraryService);
43+
readonly authorAutocomplete = inject(AuthorAutocompleteService);
4144

4245
selectedLibraryId: number | null = null;
4346
title: string = '';
@@ -51,9 +54,7 @@ export class AddPhysicalBookDialogComponent {
5154
categories: string[] = [];
5255

5356
private readonly metadata = computed(() => this.bookService.uniqueMetadata());
54-
get allAuthors(): string[] { return this.metadata().authors; }
5557
get allCategories(): string[] { return this.metadata().categories; }
56-
filteredAuthors: string[] = [];
5758
filteredCategories: string[] = [];
5859

5960
coverUrl: string | null = null;
@@ -79,13 +80,6 @@ export class AddPhysicalBookDialogComponent {
7980
return this.libraryService.libraries();
8081
}
8182

82-
filterAuthors(event: AutoCompleteCompleteEvent): void {
83-
const query = event.query.toLowerCase();
84-
this.filteredAuthors = this.allAuthors.filter((author) =>
85-
author.toLowerCase().includes(query)
86-
);
87-
}
88-
8983
filterCategories(event: AutoCompleteCompleteEvent): void {
9084
const query = event.query.toLowerCase();
9185
this.filteredCategories = this.allCategories.filter((category) =>
@@ -103,6 +97,7 @@ export class AddPhysicalBookDialogComponent {
10397
this[fieldName] = [...values, value];
10498
}
10599
input.value = '';
100+
if (fieldName === 'authors') this.authorAutocomplete.reset();
106101
}
107102
}
108103
}
@@ -113,6 +108,7 @@ export class AddPhysicalBookDialogComponent {
113108
this[fieldName] = [...values, event.value];
114109
}
115110
(event.originalEvent.target as HTMLInputElement).value = '';
111+
if (fieldName === 'authors') this.authorAutocomplete.reset();
116112
}
117113

118114
fetchMetadataByIsbn(): void {

frontend/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,14 @@
232232
class="author-add-input"
233233
[(ngModel)]="authorInputValue"
234234
[ngModelOptions]="{ standalone: true }"
235-
[suggestions]="filteredAuthors"
235+
[suggestions]="authorAutocomplete.suggestions()"
236236
[forceSelection]="false"
237237
[dropdown]="false"
238-
(completeMethod)="filterAuthors($event)"
238+
[virtualScroll]="true"
239+
[virtualScrollItemSize]="40"
240+
[virtualScrollOptions]="authorAutocomplete.virtualScrollOptions"
241+
(completeMethod)="authorAutocomplete.search($event.query)"
242+
(onClear)="resetAuthorAutocomplete()"
239243
(onKeyUp)="onAuthorInputKeyUp($event)"
240244
(onSelect)="onAuthorInputSelect($event)"
241245
placeholder="Add author..."

frontend/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.scss

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,12 @@ label {
263263
}
264264

265265
.author-add-input {
266-
flex: 1 1 5.25rem;
267-
min-width: 5.25rem;
266+
flex: 1 1 12rem;
267+
min-width: min(12rem, 100%);
268268

269269
::ng-deep input {
270+
width: 100%;
271+
min-width: 0;
270272
border: none !important;
271273
background: transparent !important;
272274
padding: 0.2188rem;

frontend/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {AppSettingsService} from '../../../../../shared/service/app-settings.ser
3232
import {MetadataProviderSpecificFields} from '../../../../../shared/model/app-settings.model';
3333
import {TranslocoDirective, TranslocoService} from '@jsverse/transloco';
3434
import {CdkDragDrop, CdkDropList, CdkDrag, moveItemInArray} from '@angular/cdk/drag-drop';
35+
import {AuthorAutocompleteService} from '../../../../author-browser/data/author-autocomplete.service';
3536

3637
@Component({
3738
selector: "app-metadata-editor",
@@ -56,6 +57,7 @@ import {CdkDragDrop, CdkDropList, CdkDrag, moveItemInArray} from '@angular/cdk/d
5657
CdkDrag,
5758
CoverComponent,
5859
],
60+
providers: [AuthorAutocompleteService],
5961
})
6062
export class MetadataEditorComponent implements OnInit {
6163
private currentBook: Book | null = null;
@@ -102,6 +104,7 @@ export class MetadataEditorComponent implements OnInit {
102104
private destroyRef = inject(DestroyRef);
103105
private appSettingsService = inject(AppSettingsService);
104106
private readonly t = inject(TranslocoService);
107+
readonly authorAutocomplete = inject(AuthorAutocompleteService);
105108
private readonly uniqueMetadata = computed(() => this.bookService.uniqueMetadata());
106109

107110
metadataForm: FormGroup;
@@ -119,14 +122,12 @@ export class MetadataEditorComponent implements OnInit {
119122

120123
originalMetadata!: BookMetadata;
121124

122-
get allAuthors(): string[] { return this.uniqueMetadata().authors; }
123125
get allCategories(): string[] { return this.uniqueMetadata().categories; }
124126
get allMoods(): string[] { return this.uniqueMetadata().moods; }
125127
get allTags(): string[] { return this.uniqueMetadata().tags; }
126128
get allPublishers(): string[] { return this.uniqueMetadata().publishers; }
127129
get allSeries(): string[] { return this.uniqueMetadata().series; }
128130
filteredCategories: string[] = [];
129-
filteredAuthors: string[] = [];
130131
authorInputValue = '';
131132
filteredMoods: string[] = [];
132133
filteredTags: string[] = [];
@@ -187,13 +188,6 @@ export class MetadataEditorComponent implements OnInit {
187188
);
188189
}
189190

190-
filterAuthors(event: { query: string }) {
191-
const query = event.query.toLowerCase();
192-
this.filteredAuthors = this.allAuthors.filter((cat) =>
193-
cat.toLowerCase().includes(query)
194-
);
195-
}
196-
197191
dropAuthor(event: CdkDragDrop<string[]>) {
198192
const authors = [...(this.metadataForm.get('authors')?.value ?? [])];
199193
moveItemInArray(authors, event.previousIndex, event.currentIndex);
@@ -217,7 +211,7 @@ export class MetadataEditorComponent implements OnInit {
217211
this.metadataForm.get('authors')?.setValue([...authors, value]);
218212
this.metadataForm.get('authors')?.markAsDirty();
219213
}
220-
this.authorInputValue = '';
214+
this.resetAuthorAutocomplete();
221215
}
222216
}
223217
}
@@ -229,7 +223,12 @@ export class MetadataEditorComponent implements OnInit {
229223
this.metadataForm.get('authors')?.setValue([...authors, value]);
230224
this.metadataForm.get('authors')?.markAsDirty();
231225
}
232-
setTimeout(() => this.authorInputValue = '');
226+
setTimeout(() => this.resetAuthorAutocomplete());
227+
}
228+
229+
resetAuthorAutocomplete() {
230+
this.authorInputValue = '';
231+
this.authorAutocomplete.reset();
233232
}
234233

235234
filterMoods(event: { query: string }) {

frontend/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,14 @@
282282
class="author-add-input"
283283
[(ngModel)]="authorInputValue"
284284
[ngModelOptions]="{ standalone: true }"
285-
[suggestions]="getFiltered('authors')"
285+
[suggestions]="authorAutocomplete.suggestions()"
286286
[forceSelection]="false"
287287
[dropdown]="false"
288-
(completeMethod)="filterItems($event, 'authors')"
288+
[virtualScroll]="true"
289+
[virtualScrollItemSize]="40"
290+
[virtualScrollOptions]="authorAutocomplete.virtualScrollOptions"
291+
(completeMethod)="authorAutocomplete.search($event.query)"
292+
(onClear)="resetAuthorAutocomplete()"
289293
(onKeyUp)="onAuthorInputKeyUp($event)"
290294
(onSelect)="onAuthorInputSelect($event)"
291295
placeholder="Add author..."

frontend/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.scss

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -640,10 +640,12 @@
640640
}
641641

642642
.author-add-input {
643-
flex: 1 1 5.25rem;
644-
min-width: 5.25rem;
643+
flex: 1 1 12rem;
644+
min-width: min(12rem, 100%);
645645

646646
::ng-deep input {
647+
width: 100%;
648+
min-width: 0;
647649
border: none !important;
648650
background: transparent !important;
649651
padding: 0.2188rem;

frontend/src/app/features/metadata/component/book-metadata-center/metadata-picker/metadata-picker.component.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {AppSettingsService} from '../../../../../shared/service/app-settings.ser
1515
import {UrlHelperService} from '../../../../../shared/service/url-helper.service';
1616
import {MetadataFormBuilder} from '../../../../../shared/metadata';
1717
import {MetadataUtilsService} from '../../../../../shared/metadata/metadata-utils.service';
18+
import {AuthorAutocompleteService} from '../../../../author-browser/data/author-autocomplete.service';
1819
import {MetadataPickerComponent} from './metadata-picker.component';
1920

2021
describe('MetadataPickerComponent', () => {
@@ -103,6 +104,7 @@ describe('MetadataPickerComponent', () => {
103104
},
104105
{provide: MessageService, useValue: {add: messageAdd}},
105106
{provide: TranslocoService, useValue: {translate}},
107+
{provide: AuthorAutocompleteService, useValue: {reset: vi.fn()}},
106108
]
107109
});
108110
});

0 commit comments

Comments
 (0)