diff --git a/frontend/src/app/document-view/document-view.component.html b/frontend/src/app/document-view/document-view.component.html index 7a043b84c..e6fac5f82 100644 --- a/frontend/src/app/document-view/document-view.component.html +++ b/frontend/src/app/document-view/document-view.component.html @@ -31,7 +31,7 @@
- +
diff --git a/frontend/src/app/document-view/document-view.component.ts b/frontend/src/app/document-view/document-view.component.ts index 6c35f700f..438f86620 100644 --- a/frontend/src/app/document-view/document-view.component.ts +++ b/frontend/src/app/document-view/document-view.component.ts @@ -1,14 +1,16 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { CorpusField, FoundDocument, Corpus, QueryModel } from '../models/index'; import { faBook, faImage } from '@fortawesome/free-solid-svg-icons'; +import { DocumentView } from '../models/document-page'; +import * as _ from 'lodash'; @Component({ selector: 'ia-document-view', templateUrl: './document-view.component.html', styleUrls: ['./document-view.component.scss'] }) -export class DocumentViewComponent { +export class DocumentViewComponent implements OnChanges { @Input() public document: FoundDocument; @@ -20,7 +22,7 @@ export class DocumentViewComponent { public corpus: Corpus; @Input() - public documentTabIndex: number; + public view: DocumentView; tabIcons = { @@ -28,6 +30,9 @@ export class DocumentViewComponent { scan: faImage, }; + /** active tab on opening */ + activeTab: string; + public imgNotFound: boolean; public imgPath: string; public media: string[]; @@ -48,6 +53,24 @@ export class DocumentViewComponent { return !!this.corpus.scan_image_type; } + ngOnChanges(changes: SimpleChanges): void { + if (changes.view) { + this.activeTab = this.tabFromView(this.view); + } + } + + /** get the tab from the view mode + * + * For "scan" view: select the scan tab if there is one + * For "content" view: select the first content field + */ + tabFromView(view: DocumentView): string { + if (view === 'scan' && this.showScanTab) { + return 'scan'; + } + return _.first(this.contentFields)['name']; + } + isUrlField(field: CorpusField) { return field.name === 'url' || field.name.startsWith('url_'); } diff --git a/frontend/src/app/document/document-popup/document-popup.component.html b/frontend/src/app/document/document-popup/document-popup.component.html new file mode 100644 index 000000000..23232638a --- /dev/null +++ b/frontend/src/app/document/document-popup/document-popup.component.html @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/frontend/src/app/document/document-popup/document-popup.component.scss b/frontend/src/app/document/document-popup/document-popup.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/document/document-popup/document-popup.component.spec.ts b/frontend/src/app/document/document-popup/document-popup.component.spec.ts new file mode 100644 index 000000000..81fae2c30 --- /dev/null +++ b/frontend/src/app/document/document-popup/document-popup.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DocumentPopupComponent } from './document-popup.component'; + +describe('DocumentPopupComponent', () => { + let component: DocumentPopupComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DocumentPopupComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DocumentPopupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/document/document-popup/document-popup.component.ts b/frontend/src/app/document/document-popup/document-popup.component.ts new file mode 100644 index 000000000..87573571b --- /dev/null +++ b/frontend/src/app/document/document-popup/document-popup.component.ts @@ -0,0 +1,67 @@ +import { Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; +import { DocumentFocus, DocumentPage, DocumentView } from '../../models/document-page'; +import { filter, takeUntil } from 'rxjs/operators'; +import * as _ from 'lodash'; +import { faArrowLeft, faArrowRight, faBookOpen, faLink } from '@fortawesome/free-solid-svg-icons'; +import { FoundDocument, QueryModel } from '../../models'; +import { Subject } from 'rxjs'; + +@Component({ + selector: 'ia-document-popup', + templateUrl: './document-popup.component.html', + styleUrls: ['./document-popup.component.scss'] +}) +export class DocumentPopupComponent implements OnChanges, OnDestroy { + @Input() page: DocumentPage; + @Input() queryModel: QueryModel; + + document: FoundDocument; + view: DocumentView; + + visible = true; + + faArrowLeft = faArrowLeft; + faArrowRight = faArrowRight; + linkIcon = faLink; + contextIcon = faBookOpen; + + private refresh$ = new Subject(); + + get documentPageLink(): string[] { + if (this.document) { + return ['/document', this.document.corpus.name, this.document.id]; + } + } + + get contextDisplayName(): string { + if (this.document.corpus.documentContext) { + return this.document.corpus.documentContext.displayName; + } + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.page) { + this.refresh$.next(); + + this.page.focus$.pipe( + takeUntil(this.refresh$), + ).subscribe(this.focusUpdate.bind(this)); + } + } + + ngOnDestroy(): void { + this.refresh$.next(); + this.refresh$.complete(); + } + + focusUpdate(focus?: DocumentFocus): void { + if (focus) { + this.document = focus.document; + this.view = focus.view; + this.visible = true; + } else { + this.document = undefined; + this.visible = false; + } + } +} diff --git a/frontend/src/app/document/document.module.ts b/frontend/src/app/document/document.module.ts index 84b110184..a75fb29de 100644 --- a/frontend/src/app/document/document.module.ts +++ b/frontend/src/app/document/document.module.ts @@ -6,6 +6,8 @@ import { ImageViewModule } from '../image-view/image-view.module'; import { SearchRelevanceComponent } from '../search'; import { CorpusModule } from '../corpus-header/corpus.module'; import { TagModule } from '../tag/tag.module'; +import { DocumentPopupComponent } from './document-popup/document-popup.component'; +import { DialogModule } from 'primeng/dialog'; @@ -14,8 +16,10 @@ import { TagModule } from '../tag/tag.module'; DocumentViewComponent, DocumentPageComponent, SearchRelevanceComponent, + DocumentPopupComponent, ], imports: [ + DialogModule, CorpusModule, SharedModule, ImageViewModule, @@ -23,6 +27,7 @@ import { TagModule } from '../tag/tag.module'; ], exports: [ DocumentViewComponent, DocumentPageComponent, + DocumentPopupComponent, SearchRelevanceComponent, ] }) diff --git a/frontend/src/app/models/document-page.ts b/frontend/src/app/models/document-page.ts new file mode 100644 index 000000000..5a94913e2 --- /dev/null +++ b/frontend/src/app/models/document-page.ts @@ -0,0 +1,50 @@ +import { Subject } from 'rxjs'; +import { FoundDocument } from './found-document'; +import { CorpusField } from './corpus'; +import * as _ from 'lodash'; + +export type DocumentView = 'content' | 'scan'; + +export interface DocumentFocus { + document: FoundDocument; + view: DocumentView; +} + +export class DocumentPage { + focus$ = new Subject(); + + constructor( + public documents: FoundDocument[], + public total: number, + public fields?: CorpusField[] + ) { + this.documents.forEach((d, i) => d.position = i + 1); + } + + focus(document: FoundDocument, view: DocumentView = 'content') { + this.focus$.next({ document, view }); + } + + focusNext(document: FoundDocument) { + this.focusShift(document, 1); + } + + focusPrevious(document: FoundDocument) { + this.focusShift(document, -1); + } + + blur() { + this.focus$.next(undefined); + } + + /** set focus to a position relative to the given document */ + private focusShift(document: FoundDocument, shift: number) { + this.focusPosition(document.position + shift); + } + + /** focus on the document at the given position in the page */ + private focusPosition(position: number) { + const index = _.clamp(position - 1, 0, this.documents.length - 1); + this.focus(this.documents[index]); + } +} diff --git a/frontend/src/app/models/page-results.ts b/frontend/src/app/models/page-results.ts new file mode 100644 index 000000000..32f46bb1e --- /dev/null +++ b/frontend/src/app/models/page-results.ts @@ -0,0 +1,58 @@ +import { Observable, combineLatest, from, of } from 'rxjs'; +import { QueryModel } from './query'; +import { map } from 'rxjs/operators'; +import { SearchService } from '../services'; +import { SearchResults } from './search-results'; +import { Results } from './results'; +import { DocumentPage } from './document-page'; + +export const RESULTS_PER_PAGE = 20; + +export interface PageResultsParameters { + from: number; + size: number; +} + +const resultsToPage = (results: SearchResults): DocumentPage => + new DocumentPage(results.documents, results.total.value, results.fields); + +export class PageResults extends Results { + from$: Observable; + to$: Observable; + + constructor( + private searchService: SearchService, + query: QueryModel, + ) { + super(query, { + from: 0, + size: RESULTS_PER_PAGE, + }); + this.from$ = this.parameters$.pipe( + map(parameters => parameters.from + 1) + ); + this.to$ = combineLatest([this.parameters$, this.result$]).pipe( + map(this.highestDocumentIndex) + ); + } + + /** Parameters to re-assign when the query model is updated. */ + assignOnQueryUpdate(): Partial { + return { + from: 0 + }; + } + + fetch(params: PageResultsParameters): Observable { + return from(this.searchService.loadResults( + this.query, params.from, params.size + )).pipe( + map(resultsToPage) + ); + } + + private highestDocumentIndex([parameters, result]: [PageResultsParameters, DocumentPage]): number { + const limit = parameters.from + parameters.size; + return Math.min(limit, result.total); + } +} diff --git a/frontend/src/app/models/query.spec.ts b/frontend/src/app/models/query.spec.ts index 5e1464959..beca2cb63 100644 --- a/frontend/src/app/models/query.spec.ts +++ b/frontend/src/app/models/query.spec.ts @@ -1,10 +1,9 @@ import { mockField2, mockFieldDate, mockFieldMultipleChoice } from '../../mock-data/corpus'; import { Corpus, } from './corpus'; import { QueryModel } from './query'; -import { DateFilter, MultipleChoiceFilter, SearchFilter } from './field-filter'; +import { SearchFilter } from './field-filter'; import { convertToParamMap } from '@angular/router'; import * as _ from 'lodash'; -import { paramsHaveChanged } from '../utils/params'; const corpus: Corpus = { name: 'mock-corpus', diff --git a/frontend/src/app/models/query.ts b/frontend/src/app/models/query.ts index 5188fc127..d91f580b1 100644 --- a/frontend/src/app/models/query.ts +++ b/frontend/src/app/models/query.ts @@ -1,6 +1,5 @@ import { convertToParamMap, ParamMap } from '@angular/router'; -import * as _ from 'lodash'; -import { combineLatest, Subject } from 'rxjs'; +import { Subject } from 'rxjs'; import { Corpus, CorpusField, EsFilter, SortBy, SortConfiguration, SortDirection, } from '../models/index'; import { EsQuery } from '../models'; import { combineSearchClauseAndFilters, makeHighlightSpecification } from '../utils/es-query'; diff --git a/frontend/src/app/models/results.ts b/frontend/src/app/models/results.ts new file mode 100644 index 000000000..969554594 --- /dev/null +++ b/frontend/src/app/models/results.ts @@ -0,0 +1,98 @@ +import { BehaviorSubject, Observable, Subject, merge, of } from 'rxjs'; +import { QueryModel } from './query'; +import { catchError, map, mergeMap, share, shareReplay, takeUntil, tap } from 'rxjs/operators'; +import * as _ from 'lodash'; + +/** + * Abstract class for any kind of results based on a query model + * + * Child classes can configure additional parameters, and the method + * for fetching results. Results will be loaded when the query model + * or the parameters update + */ +export abstract class Results { + /** additional parameters besides the query model */ + parameters$: BehaviorSubject; + + /** retrieved results */ + result$: Observable; + + /** errors thrown in the last results fetch (if any) */ + error$: BehaviorSubject; + + /** whether the model is currently loading results; + * can be used to show a loading spinner + */ + loading$: Observable; + + private complete$ = new Subject(); + + constructor( + public query: QueryModel, + initialParameters: Parameters, + ) { + this.error$ = new BehaviorSubject(undefined); + this.parameters$ = new BehaviorSubject(initialParameters); + + this.query.update.pipe( + takeUntil(this.complete$), + map(this.assignOnQueryUpdate.bind(this)), + ).subscribe((params: Partial) => + this.setParameters(params) + ); + + this.result$ = this.parameters$.pipe( + takeUntil(this.complete$), + mergeMap(this.fetch.bind(this)), + catchError(err => { + this.error$.next(err); + return of(undefined); + }), + shareReplay(1), + ); + + this.loading$ = this.makeLoadingObservable(); + } + + /** Parameters to re-assign when the query model is updated. */ + assignOnQueryUpdate(): Partial { + return {}; + } + + /** Set parameters. + * + * The new value can be partial; it will be merged with the current parameters. + * Updating parameters will trigger new results being fetched. + */ + setParameters(newValues: Partial) { + this.error$.next(undefined); + const params: Parameters = _.assign(this.parameters$.value, newValues); + this.parameters$.next(params); + } + + /** + * stops the results object from listening to the query model, + * and completes observables + */ + complete() { + this.complete$.next(); + this.parameters$.complete(); + this.error$.complete(); + this.complete$.complete(); + } + + /** set up the loading$ observable */ + private makeLoadingObservable(): Observable { + const onQueryUpdate$ = this.query.update.pipe(map(() => true)); + const onParameterChange$ = this.parameters$.pipe(map(() => true)); + const onResult$ = this.result$.pipe(map(() => false)); + const onError$ = this.error$.pipe(map(() => false)); + return merge(onQueryUpdate$, onParameterChange$, onResult$, onError$); + } + + /** fetch results */ + abstract fetch(parameters: Parameters): Observable; + +} + + diff --git a/frontend/src/app/search/pagination/pagination.component.ts b/frontend/src/app/search/pagination/pagination.component.ts index a67cf5fb7..7734ba3df 100644 --- a/frontend/src/app/search/pagination/pagination.component.ts +++ b/frontend/src/app/search/pagination/pagination.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; -import { SearchParameters } from '../../models/index'; +import { PageResultsParameters } from '../../models/page-results'; @Component({ selector: 'ia-pagination', @@ -16,8 +16,8 @@ export class PaginationComponent implements OnChanges { public currentPages: number[]; public currentPage: number; - @Output('loadResults') - public loadResultsEvent = new EventEmitter(); + @Output('parameters') + public loadResultsEvent = new EventEmitter(); constructor() { } diff --git a/frontend/src/app/search/search-results.component.html b/frontend/src/app/search/search-results.component.html index 6da05070c..2f11fbfe0 100644 --- a/frontend/src/app/search/search-results.component.html +++ b/frontend/src/app/search/search-results.component.html @@ -1,127 +1,95 @@ - -

- 0 hits -

-
- - +
-

- {results.total.value, plural, =1 {1 hit} other {{{results.total.value}} hits}} +

+ {page.total, plural, =1 {1 hit} other {{{page.total}} hits}}

-

- {{results.total.value}} results. - Showing results {{fromIndex+1}} - {{fromIndex+resultsPerPage > totalResults? totalResults : fromIndex+resultsPerPage}}: +

+ {{page.total}} results. + Showing results {{pageResults.from$ | async}} + - + {{(pageResults.to$ | async)}}

- +
- -
- -
-
-

Sort By

- -
-
-
-
-

Pages

- -
+
+
+
+

Sort By

+ +
+
+
+
+

Pages

+ +
+
-
-
-
-
-
- - - - - - - - - - - + +
+
+
+
+
+
- {{field.displayName}}: - - -
- -
-
-
+ + + + + + + + + + + +
+ {{field.displayName}}: + + +
+
+
+
+
+
+ +
+ Relevance: {{document.relevance * 100 | number:'1.0-0' }}% +
+
- + +
-
- -
- Relevance: {{document.relevance * 100 | number:'1.0-0' }}% -
- -
- -
-
+
-
+ + + + + + +
+ No results +
- - - - - - - + + + - + +
+
diff --git a/frontend/src/app/search/search-results.component.spec.ts b/frontend/src/app/search/search-results.component.spec.ts index 60569c692..db0d42c03 100644 --- a/frontend/src/app/search/search-results.component.spec.ts +++ b/frontend/src/app/search/search-results.component.spec.ts @@ -7,7 +7,45 @@ import { CorpusField, FoundDocument, QueryModel } from '../models/index'; import { SearchResultsComponent } from './search-results.component'; import { makeDocument } from '../../mock-data/constructor-helpers'; +import { PageResults } from '../models/page-results'; +import { DocumentPage } from '../models/document-page'; +import { of } from 'rxjs'; +import { By } from '@angular/platform-browser'; +const createField = (name: string): CorpusField => { + const field = _.cloneDeep(mockField); + field.name = name; + return field; +}; + +const documents: FoundDocument[] = [ + makeDocument( + { + a: '1', + b: '2', + c: 'Hide-and-seek!' + }, mockCorpus, '1', 1, + { + c: ['Where is Wally?', 'I cannot find Wally anywhere!'] + } + ), + makeDocument( + { + a: '3', + b: '4', + c: 'Wally is here' + }, mockCorpus, '2', 0.5 + ) +]; + +const fields = ['a', 'b', 'c'].map(createField); + +class MockResults extends PageResults { + fetch() { + const page = new DocumentPage(documents, 2, fields); + return of(page); + } +} describe('Search Results Component', () => { let component: SearchResultsComponent; @@ -19,52 +57,44 @@ describe('Search Results Component', () => { beforeEach(() => { fixture = TestBed.createComponent(SearchResultsComponent); - const fields = ['a', 'b', 'c'].map(createField); + component = fixture.componentInstance; - component.results = { - fields, - documents: [makeDocument({ - a: '1', - b: '2', - c: 'Hide-and-seek!' - }, mockCorpus, '1', 1, - { - c: ['Where is Wally?', 'I cannot find Wally anywhere!'] - }), - makeDocument({ - a: '3', - b: '4', - c: 'Wally is here' - }, mockCorpus, '2', 0.5)], - total: { - value: 2, - relation: 'gte' - } - }; - component.corpus = _.merge(mockCorpus, fields); - component.fromIndex = 0; - component.resultsPerPage = 20; - const query = new QueryModel(component.corpus); + }); + + beforeEach(() => { + const corpus = _.merge(mockCorpus, fields); + const query = new QueryModel(corpus); query.setQueryText('wally'); query.setHighlight(10); component.queryModel = query; fixture.detectChanges(); + component.pageResults = new MockResults(undefined, component.queryModel); }); - const createField = (name: string): CorpusField => { - const field = _.cloneDeep(mockField); - field.name = name; - return field; - }; it('should be created', () => { expect(component).toBeTruthy(); }); + it('should show a loading spinner on opening', () => { + const loadingElement = fixture.debugElement.query( + By.css('.is-loading') + ); + expect(loadingElement).toBeTruthy(); + }); + it('should render result', async () => { await fixture.whenStable(); - const compiled = fixture.debugElement.nativeElement; - expect(compiled.innerHTML).toContain('Wally is here'); + fixture.detectChanges(); + + const element = fixture.debugElement; + + const docs = element.queryAll( + By.css('article') + ); + expect(docs.length).toBe(2); + + expect(element.nativeElement.innerHTML).toContain('Wally is here'); }); }); diff --git a/frontend/src/app/search/search-results.component.ts b/frontend/src/app/search/search-results.component.ts index 88df3cf46..8b21f002b 100644 --- a/frontend/src/app/search/search-results.component.ts +++ b/frontend/src/app/search/search-results.component.ts @@ -1,12 +1,13 @@ /* eslint-disable @typescript-eslint/member-ordering */ -import { Component, ElementRef, EventEmitter, HostListener, Input, OnChanges, Output, ViewChild } from '@angular/core'; +import { Component, ElementRef, EventEmitter, HostListener, Input, OnChanges, OnDestroy, Output, SimpleChanges, ViewChild } from '@angular/core'; -import { User, Corpus, SearchParameters, SearchResults, FoundDocument, QueryModel, ResultOverview } from '../models/index'; +import { User, SearchResults, FoundDocument, QueryModel, ResultOverview } from '../models/index'; import { SearchService } from '../services'; import { ShowError } from '../error/error.component'; -import * as _ from 'lodash'; -import { faBookOpen, faArrowLeft, faArrowRight, faLink } from '@fortawesome/free-solid-svg-icons'; -import { makeContextParams } from '../utils/document-context'; +import { PageResultsParameters, PageResults } from '../models/page-results'; +import { Observable, Subject } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; +import { DocumentPage } from '../models/document-page'; const MAXIMUM_DISPLAYED = 10000; @@ -15,7 +16,7 @@ const MAXIMUM_DISPLAYED = 10000; templateUrl: './search-results.component.html', styleUrls: ['./search-results.component.scss'] }) -export class SearchResultsComponent implements OnChanges { +export class SearchResultsComponent implements OnChanges, OnDestroy { @ViewChild('resultsNavigation', {static: true}) public resultsNavigation: ElementRef; @@ -28,60 +29,63 @@ export class SearchResultsComponent implements OnChanges { @Input() public user: User; - @Input() - public corpus: Corpus; - - @Input() - public parentElement: HTMLElement; - - @Output('view') - public viewEvent = new EventEmitter<{document: FoundDocument; tabIndex?: number}>(); - @Output('searched') public searchedEvent = new EventEmitter(); + public pageResults: PageResults; + public isLoading = false; public isScrolledDown: boolean; public results: SearchResults; public resultsPerPage = 20; - public totalResults: number; - - public fromIndex = 0; public imgSrc: Uint8Array; - /** - * For failed searches. - */ - public showError: false | undefined | ShowError; + error$: Observable; - /** - * Whether a document has been selected to be shown. - */ - public showDocument = false; - /** - * The document to view separately. - */ - public viewDocument: FoundDocument; + /** tab on which the focused document should be opened */ public documentTabIndex: number; - contextIcon = faBookOpen; - linkIcon = faLink; - faArrowLeft = faArrowLeft; - faArrowRight = faArrowRight; + private destroy$ = new Subject(); constructor(private searchService: SearchService) { } - ngOnChanges() { - if (this.queryModel) { - this.fromIndex = 0; - this.search(); - this.queryModel.update.subscribe(() => this.search()); + ngOnChanges(changes: SimpleChanges) { + if (changes.queryModel) { + this.pageResults?.complete(); + this.pageResults = new PageResults(this.searchService, this.queryModel); + this.error$ = this.pageResults.error$.pipe( + map(this.parseError) + ); + this.pageResults.result$.pipe( + takeUntil(this.destroy$) + ).subscribe(result => { + this.searchedEvent.emit({ queryText: this.queryModel.queryText, resultsCount: result.total }); + }); } } + ngOnDestroy(): void { + this.pageResults?.complete(); + this.destroy$.next(); + this.destroy$.complete(); + } + + setParameters(parameters: PageResultsParameters) { + this.pageResults?.setParameters(parameters); + } + + totalDisplayed(totalResults: number) { + return Math.min(totalResults, MAXIMUM_DISPLAYED); + } + + goToScan(page: DocumentPage, document: FoundDocument, event: Event) { + page.focus(document, 'scan'); + event.stopPropagation(); + }; + @HostListener('window:scroll', []) onWindowScroll() { // mark that the search results were scrolled down beyond 68 pixels from top (position underneath sticky search bar) @@ -91,95 +95,13 @@ export class SearchResultsComponent implements OnChanges { } } - private search() { - this.isLoading = true; - this.searchService.search(this.queryModel).then(results => { - this.results = results; - this.results.documents.map((d, i) => d.position = i + 1); - this.searched(this.queryModel.queryText, this.results.total.value); - this.totalResults = this.results.total.value <= MAXIMUM_DISPLAYED ? this.results.total.value : MAXIMUM_DISPLAYED; - }, error => { - this.showError = { + private parseError(error): ShowError { + if (error) { + return { date: (new Date()).toISOString(), href: location.href, message: error.message || 'An unknown error occurred' }; - console.trace(error); - // if an error occurred, return query text and 0 results - this.searched(this.queryModel.queryText, 0); - }); - } - - public async loadResults(searchParameters: SearchParameters) { - this.isLoading = true; - this.fromIndex = searchParameters.from; - this.resultsPerPage = searchParameters.size; - this.results = await this.searchService.loadResults(this.queryModel, searchParameters.from, searchParameters.size); - this.results.documents.map( (d, i) => d.position = i + searchParameters.from + 1 ); - this.isLoading = false; - } - - public searched(queryText: string, resultsCount: number) { - // emit searchedEvent to search component - this.searchedEvent.next({ queryText, resultsCount }); - this.isLoading = false; - } - - public goToScan(document: FoundDocument, event: any) { - this.onViewDocument(document); - this.documentTabIndex = 1; - event.stopPropagation(); - } - - public onViewDocument(document: FoundDocument) { - this.showDocument = true; - this.viewDocument = document; - this.documentTabIndex = 0; - } - - get contextDisplayName(): string { - if (this.corpus && this.corpus.documentContext) { - return this.corpus.documentContext.displayName; } } - - public async nextDocument(document: FoundDocument) { - const newPosition = document.position + 1; - const maxPosition = this.fromIndex + this.results.documents.length; - - if (newPosition > maxPosition) { - this.fromIndex = maxPosition + 1; - await this.loadResults({ - from: maxPosition, - size: this.resultsPerPage, - }); - this.viewDocumentAtPosition(newPosition); - } else { - this.viewDocumentAtPosition(newPosition); - } - } - - public async prevDocument(document: FoundDocument) { - const newPosition = document.position - 1; - const minPosition = this.fromIndex + 1; - - if (newPosition < minPosition) { - this.fromIndex = this.fromIndex - this.resultsPerPage; - await this.loadResults({ - from: this.fromIndex, - size: this.resultsPerPage, - }); - this.viewDocumentAtPosition(newPosition); - } else { - this.viewDocumentAtPosition(newPosition); - } - } - - viewDocumentAtPosition(position: number) { - const document = this.results.documents.find(doc => - doc.position === position - ); - this.onViewDocument(document); - } - } diff --git a/frontend/src/app/search/search.component.html b/frontend/src/app/search/search.component.html index 7e95bfefe..e3e782532 100644 --- a/frontend/src/app/search/search.component.html +++ b/frontend/src/app/search/search.component.html @@ -55,10 +55,8 @@
- - - diff --git a/frontend/src/app/search/search.module.ts b/frontend/src/app/search/search.module.ts index c4e62a79e..b1f72d613 100644 --- a/frontend/src/app/search/search.module.ts +++ b/frontend/src/app/search/search.module.ts @@ -9,7 +9,6 @@ import { CorpusModule } from '../corpus-header/corpus.module'; import { SearchSortingComponent } from './search-sorting.component'; import { FilterModule } from '../filter/filter.module'; import { DownloadModule } from '../download/download.module'; -import { DialogModule } from 'primeng/dialog'; import { QueryService, SearchService } from '../services'; import { VisualizationModule } from '../visualization/visualization.module'; @@ -28,7 +27,6 @@ import { VisualizationModule } from '../visualization/visualization.module'; SearchSortingComponent, ], imports: [ - DialogModule, CorpusModule, DocumentModule, DownloadModule, diff --git a/frontend/src/app/services/elastic-search.service.spec.ts b/frontend/src/app/services/elastic-search.service.spec.ts index b24021381..bc806258c 100644 --- a/frontend/src/app/services/elastic-search.service.spec.ts +++ b/frontend/src/app/services/elastic-search.service.spec.ts @@ -79,7 +79,7 @@ describe('ElasticSearchService', () => { it('should make a search request', async () => { const queryModel = new QueryModel(mockCorpus); const size = 2; - const response = service.search(queryModel, size); + const response = service.loadResults(queryModel, 0, size); const searchUrl = `/api/es/${mockCorpus.name}/_search?size=${size}`; httpTestingController.expectOne(searchUrl).flush(mockResponse); diff --git a/frontend/src/app/services/elastic-search.service.ts b/frontend/src/app/services/elastic-search.service.ts index be8d30354..2ba4d6d5e 100644 --- a/frontend/src/app/services/elastic-search.service.ts +++ b/frontend/src/app/services/elastic-search.service.ts @@ -9,13 +9,12 @@ import { import * as _ from 'lodash'; import { TagService } from './tag.service'; import { QueryParameters } from '../models/search-requests'; +import { RESULTS_PER_PAGE } from '../models/page-results'; @Injectable() export class ElasticSearchService { - private resultsPerPage = 20; - constructor(private http: HttpClient, private tagService: TagService) { } @@ -79,29 +78,17 @@ export class ElasticSearchService { }; } - - - public async search( - queryModel: QueryModel, - size?: number, - ): Promise { - const esQuery = queryModel.toEsQuery(); - - // Perform the search - const response = await this.execute(queryModel.corpus, esQuery, size || this.resultsPerPage); - return this.parseResponse(queryModel.corpus, response); - } - - /** * Load results for requested page */ public async loadResults( - queryModel: QueryModel, from: number, - size: number): Promise { + queryModel: QueryModel, + from: number, + size: number = RESULTS_PER_PAGE + ): Promise { const esQuery = queryModel.toEsQuery(); // Perform the search - const response = await this.execute(queryModel.corpus, esQuery, size || this.resultsPerPage, from); + const response = await this.execute(queryModel.corpus, esQuery, size, from); return this.parseResponse(queryModel.corpus, response); } diff --git a/frontend/src/app/services/search.service.spec.ts b/frontend/src/app/services/search.service.spec.ts index 0b4e67275..fe20957c1 100644 --- a/frontend/src/app/services/search.service.spec.ts +++ b/frontend/src/app/services/search.service.spec.ts @@ -45,7 +45,7 @@ describe('SearchService', () => { it('should search', inject([SearchService], async (service: SearchService) => { const queryModel = new QueryModel(mockCorpus); - const results = await service.search(queryModel); + const results = await service.loadResults(queryModel, 0, 20); expect(results).toBeTruthy(); expect(results.total.value).toBeGreaterThan(0); })); diff --git a/frontend/src/app/services/search.service.ts b/frontend/src/app/services/search.service.ts index 455fe48eb..8d04a08a0 100644 --- a/frontend/src/app/services/search.service.ts +++ b/frontend/src/app/services/search.service.ts @@ -30,16 +30,7 @@ export class SearchService { from, size ); - results.fields = queryModel.corpus.fields.filter((field) => field.resultsOverview); - return results; - } - - public async search(queryModel: QueryModel - ): Promise { - const request = this.elasticSearchService.search(queryModel); - return request.then(results => - this.filterResultsFields(results, queryModel) - ); + return this.filterResultsFields(results, queryModel); } public async aggregateSearch( diff --git a/frontend/src/mock-data/elastic-search.ts b/frontend/src/mock-data/elastic-search.ts index 4a983a201..c584514ad 100644 --- a/frontend/src/mock-data/elastic-search.ts +++ b/frontend/src/mock-data/elastic-search.ts @@ -12,7 +12,7 @@ export class ElasticSearchServiceMock { return Promise.resolve(makeDocument({content: 'Hello world!'})); } - search(): Promise { + loadResults(): Promise { return Promise.resolve({ total: { relation: 'eq',