Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/results model #1307

Merged
merged 32 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
10f7eef
scaffold results base class
lukavdplas Nov 1, 2023
bd0c78e
add esQuery observable
lukavdplas Nov 1, 2023
585ce6d
draft PageResults
lukavdplas Nov 1, 2023
3793008
generate document popup component
lukavdplas Nov 1, 2023
7462fff
draft model-based results template
lukavdplas Nov 1, 2023
9a187a6
enable next/prev document buttons
lukavdplas Nov 1, 2023
f53879a
show page and context link
lukavdplas Nov 1, 2023
165441a
restore highlight, sort and total
lukavdplas Nov 1, 2023
f5f6d17
change section scope
lukavdplas Nov 1, 2023
2de499c
include pagination feedback
lukavdplas Nov 1, 2023
24d2ae8
remove unused code
lukavdplas Nov 1, 2023
4107349
show errors
lukavdplas Nov 1, 2023
690e0a6
show loading spinner
lukavdplas Nov 2, 2023
3292677
maximum page
lukavdplas Nov 2, 2023
36d4b82
emit total documents
lukavdplas Nov 2, 2023
85ecf40
separate file for page results
lukavdplas Nov 2, 2023
fc1d972
reset page on query updates
lukavdplas Nov 2, 2023
5c7363c
simplify code
lukavdplas Nov 2, 2023
502a8a9
fix search results spec
lukavdplas Nov 2, 2023
26a30b2
shareReplay results observable
lukavdplas Nov 2, 2023
914cd7c
remove view event
lukavdplas Nov 2, 2023
7209873
reorganising
lukavdplas Nov 2, 2023
5c4d1ef
view scan
lukavdplas Nov 2, 2023
e7ab8fa
remove duplicate bit in template
lukavdplas Nov 2, 2023
e4b0a62
code cleanup
lukavdplas Nov 2, 2023
d98950e
small fixes
lukavdplas Nov 2, 2023
e958ebd
use th for row headers
lukavdplas Nov 2, 2023
6069ca8
clarify active tab functionality
lukavdplas Nov 23, 2023
267441c
fix leaky subscription
lukavdplas Nov 23, 2023
800efcc
clarifications; add complete method
lukavdplas Nov 23, 2023
a00d5d4
avoid leaky subscriptions
lukavdplas Nov 23, 2023
71602a0
Merge branch 'develop' into feature/results-model
lukavdplas Nov 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

<div class="column is-7">
<div class="box">
<ia-tabs>
<ia-tabs [activeTab]="activeTab">
<ng-template iaTabPanel *ngFor="let field of contentFields" [id]="field.name" [title]="field.displayName" [icon]="tabIcons.text">
<div class="content" [innerHtml]="highlightedInnerHtml(field)"></div>
</ng-template>
Expand Down
17 changes: 14 additions & 3 deletions frontend/src/app/document-view/document-view.component.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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';

@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;
Expand All @@ -20,14 +21,16 @@ export class DocumentViewComponent {
public corpus: Corpus;

@Input()
public documentTabIndex: number;
public view: DocumentView;


tabIcons = {
text: faBook,
scan: faImage,
};

activeTab: 'scan' | undefined;

lukavdplas marked this conversation as resolved.
Show resolved Hide resolved
public imgNotFound: boolean;
public imgPath: string;
public media: string[];
Expand All @@ -48,6 +51,14 @@ export class DocumentViewComponent {
return !!this.corpus.scan_image_type;
}

ngOnChanges(changes: SimpleChanges): void {
if (changes.view) {
if (this.view === 'scan' && this.showScanTab) {
this.activeTab = 'scan';
}
}
}

isUrlField(field: CorpusField) {
return field.name === 'url' || field.name.startsWith('url_');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<ng-container *ngIf="document">
<p-dialog [(visible)]="visible" width="100%"
[responsive]="true" [maximizable]="true" [dismissableMask]="true" [draggable]="true" [resizable]="false" [blockScroll]="true"
header="Document {{document.position}} of {{page.total}}">

<ia-document-view [document]="document" [queryModel]="queryModel" [corpus]="document.corpus" [view]="view"></ia-document-view>

<ng-template pTemplate="footer">
<div class="columns" style="text-align:left">
<div class="column">
<a *ngIf="document.position > 1"
iaBalloon="view previous document" iaBalloonPosition="right"
(click)="page.focusPrevious(document)" (keydown.enter)="page.focusPrevious(document)"
role="button" tabindex="0">
<span class="icon"><fa-icon [icon]="faArrowLeft">previous</fa-icon></span>
</a>
</div>
<div class="column" style="text-align:center">
<a [routerLink]="documentPageLink"
iaBalloon="view this document on its own page" autofocus
tabindex="0">
<span class="icon">
<fa-icon [icon]="linkIcon"></fa-icon>
</span>
<span>Link</span>
</a>
&nbsp;
<a *ngIf="document.hasContext"
[routerLink]="['/search', document.corpus.name]" [queryParams]="document.contextQueryParams"
iaBalloon="view all documents from this {{contextDisplayName}}"
tabindex="0">
<span class="icon">
<fa-icon [icon]="contextIcon"></fa-icon>
</span>
<span>View {{contextDisplayName}}</span>
</a>
</div>
<div class="column" style="text-align:right">
<a *ngIf="document.position < page.documents.length"
iaBalloon="view next document" iaBalloonPosition="left"
(click)="page.focusNext(document)" (keydown.enter)="page.focusNext(document)"
role="button" tabindex="0">
<span class="icon"><fa-icon [icon]="faArrowRight">next</fa-icon></span>
</a>
</div>
</div>
</ng-template>
</p-dialog>
</ng-container>
Empty file.
Original file line number Diff line number Diff line change
@@ -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<DocumentPopupComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DocumentPopupComponent]
})
.compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(DocumentPopupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { DocumentPage, DocumentView } from '../../models/document-page';
import { filter } from 'rxjs/operators';
import * as _ from 'lodash';
import { faArrowLeft, faArrowRight, faBookOpen, faLink } from '@fortawesome/free-solid-svg-icons';
import { FoundDocument, QueryModel } from '../../models';

@Component({
selector: 'ia-document-popup',
templateUrl: './document-popup.component.html',
styleUrls: ['./document-popup.component.scss']
})
export class DocumentPopupComponent implements OnChanges {
@Input() page: DocumentPage;
@Input() queryModel: QueryModel;

document: FoundDocument;
view: DocumentView;

visible = true;

faArrowLeft = faArrowLeft;
faArrowRight = faArrowRight;
linkIcon = faLink;
contextIcon = faBookOpen;

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.page.focus$.pipe(
filter(focus => !!focus),
).subscribe(focus => {
lukavdplas marked this conversation as resolved.
Show resolved Hide resolved
this.document = focus.document;
this.view = focus.view;
this.visible = true;
});
}
}
}
5 changes: 5 additions & 0 deletions frontend/src/app/document/document.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';



Expand All @@ -14,15 +16,18 @@ import { TagModule } from '../tag/tag.module';
DocumentViewComponent,
DocumentPageComponent,
SearchRelevanceComponent,
DocumentPopupComponent,
],
imports: [
DialogModule,
CorpusModule,
SharedModule,
ImageViewModule,
TagModule,
], exports: [
DocumentViewComponent,
DocumentPageComponent,
DocumentPopupComponent,
SearchRelevanceComponent,
]
})
Expand Down
48 changes: 48 additions & 0 deletions frontend/src/app/models/document-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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<DocumentFocus>();

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);
}

private focusShift(document: FoundDocument, shift: number) {
this.focusPosition(document.position + shift);
}

private focusPosition(position: number) {
const index = _.clamp(position - 1, 0, this.documents.length - 1);
this.focus(this.documents[index]);
}
}
Comment on lines +46 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method could use a (short) docstring

57 changes: 57 additions & 0 deletions frontend/src/app/models/page-results.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How often do we expect to change this value? Could it be variable for different flavours of I-Analyzer? If so, this may belong in environment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, good one. I think the more conventional way of handling it is to put a control for this next to pagination, so let users decide. But if I was going to work on expanding the results page right now, I would prioritise #1011, #1012, #729, or #996 - a size control seems low-priority to me. It may bring more clutter than value.

As for adding it to environment settings: that is technically trivial, but then I don't think have a reason to change this between our existing environments. Perhaps we could think more long-term about what we envision for different I-analyzer instances.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stray thought on this: size=20 is a bit trivial, since that is the elasticsearch default. Perhaps if we did not specify this, it could also be configured in the elasticsearch settings? Which would also make it environment-configurable.


export interface PageResultsParameters {
from: number;
size: number;
}

const parseResults = (results: SearchResults): DocumentPage =>
lukavdplas marked this conversation as resolved.
Show resolved Hide resolved
new DocumentPage(results.documents, results.total.value, results.fields);

export class PageResults extends Results<PageResultsParameters, DocumentPage> {
from$: Observable<number>;
to$: Observable<number>;

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)
);
}

assignOnQueryUpdate(): Partial<PageResultsParameters> {
return {
from: 0
};
}
Comment on lines +40 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a docstring


fetch(params: PageResultsParameters): Observable<DocumentPage> {
return from(this.searchService.loadResults(
this.query, params.from, params.size
)).pipe(
map(parseResults)
);
}

private highestDocumentIndex([parameters, result]: [PageResultsParameters, DocumentPage]): number {
const limit = parameters.from + parameters.size;
return Math.min(limit, result.total);
}
}
3 changes: 1 addition & 2 deletions frontend/src/app/models/query.spec.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/app/models/query.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Loading
Loading