Skip to content

Commit

Permalink
Merge pull request #1307 from UUDigitalHumanitieslab/feature/results-…
Browse files Browse the repository at this point in the history
…model

Feature/results model
  • Loading branch information
lukavdplas authored Nov 24, 2023
2 parents 9224528 + 71602a0 commit 33a4953
Show file tree
Hide file tree
Showing 23 changed files with 584 additions and 317 deletions.
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
29 changes: 26 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,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;
Expand All @@ -20,14 +22,17 @@ export class DocumentViewComponent {
public corpus: Corpus;

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


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

/** active tab on opening */
activeTab: string;

public imgNotFound: boolean;
public imgPath: string;
public media: string[];
Expand All @@ -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_');
}
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,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<void>();

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;
}
}
}
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
50 changes: 50 additions & 0 deletions frontend/src/app/models/document-page.ts
Original file line number Diff line number Diff line change
@@ -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<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);
}

/** 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]);
}
}
58 changes: 58 additions & 0 deletions frontend/src/app/models/page-results.ts
Original file line number Diff line number Diff line change
@@ -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<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)
);
}

/** Parameters to re-assign when the query model is updated. */
assignOnQueryUpdate(): Partial<PageResultsParameters> {
return {
from: 0
};
}

fetch(params: PageResultsParameters): Observable<DocumentPage> {
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);
}
}
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

0 comments on commit 33a4953

Please sign in to comment.