mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	lots of changes for the new unified search
This commit is contained in:
		| @@ -10,7 +10,6 @@ import { LogsComponent } from './components/manage/logs/logs.component'; | ||||
| import { SettingsComponent } from './components/manage/settings/settings.component'; | ||||
| import { TagListComponent } from './components/manage/tag-list/tag-list.component'; | ||||
| import { NotFoundComponent } from './components/not-found/not-found.component'; | ||||
| import { SearchComponent } from './components/search/search.component'; | ||||
|  | ||||
| const routes: Routes = [ | ||||
|   {path: '', redirectTo: 'dashboard', pathMatch: 'full'}, | ||||
| @@ -18,7 +17,6 @@ const routes: Routes = [ | ||||
|     {path: 'dashboard', component: DashboardComponent }, | ||||
|     {path: 'documents', component: DocumentListComponent }, | ||||
|     {path: 'view/:id', component: DocumentListComponent }, | ||||
|     {path: 'search', component: SearchComponent }, | ||||
|     {path: 'documents/:id', component: DocumentDetailComponent }, | ||||
|    | ||||
|     {path: 'tags', component: TagListComponent }, | ||||
|   | ||||
| @@ -21,8 +21,6 @@ import { CorrespondentEditDialogComponent } from './components/manage/correspond | ||||
| import { TagEditDialogComponent } from './components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component'; | ||||
| import { DocumentTypeEditDialogComponent } from './components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component'; | ||||
| import { TagComponent } from './components/common/tag/tag.component'; | ||||
| import { SearchComponent } from './components/search/search.component'; | ||||
| import { ResultHighlightComponent } from './components/search/result-highlight/result-highlight.component'; | ||||
| import { PageHeaderComponent } from './components/common/page-header/page-header.component'; | ||||
| import { AppFrameComponent } from './components/app-frame/app-frame.component'; | ||||
| import { ToastsComponent } from './components/common/toasts/toasts.component'; | ||||
| @@ -104,8 +102,6 @@ registerLocaleData(localeEs) | ||||
|     TagEditDialogComponent, | ||||
|     DocumentTypeEditDialogComponent, | ||||
|     TagComponent, | ||||
|     SearchComponent, | ||||
|     ResultHighlightComponent, | ||||
|     PageHeaderComponent, | ||||
|     AppFrameComponent, | ||||
|     ToastsComponent, | ||||
|   | ||||
| @@ -10,6 +10,8 @@ import { SearchService } from 'src/app/services/rest/search.service'; | ||||
| import { environment } from 'src/environments/environment'; | ||||
| import { DocumentDetailComponent } from '../document-detail/document-detail.component'; | ||||
| import { Meta } from '@angular/platform-browser'; | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service'; | ||||
| import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-app-frame', | ||||
| @@ -24,6 +26,7 @@ export class AppFrameComponent implements OnInit { | ||||
|     private openDocumentsService: OpenDocumentsService, | ||||
|     private searchService: SearchService, | ||||
|     public savedViewService: SavedViewService, | ||||
|     private list: DocumentListViewService, | ||||
|     private meta: Meta | ||||
|     ) { | ||||
|  | ||||
| @@ -74,7 +77,7 @@ export class AppFrameComponent implements OnInit { | ||||
|  | ||||
|   search() { | ||||
|     this.closeMenu() | ||||
|     this.router.navigate(['search'], {queryParams: {query: this.searchField.value}}) | ||||
|     this.list.quickFilter([{rule_type: FILTER_FULLTEXT_QUERY, value: this.searchField.value}]) | ||||
|   } | ||||
|  | ||||
|   closeDocument(d: PaperlessDocument) { | ||||
|   | ||||
| @@ -20,6 +20,7 @@ import { ToastService } from 'src/app/services/toast.service'; | ||||
| import { TextComponent } from '../common/input/text/text.component'; | ||||
| import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service'; | ||||
| import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions'; | ||||
| import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-document-detail', | ||||
| @@ -219,7 +220,7 @@ export class DocumentDetailComponent implements OnInit { | ||||
|   } | ||||
|  | ||||
|   moreLike() { | ||||
|     this.router.navigate(["search"], {queryParams: {more_like:this.document.id}}) | ||||
|     this.documentListViewService.quickFilter([{rule_type: FILTER_FULLTEXT_MORELIKE, value: this.documentId.toString()}]) | ||||
|   } | ||||
|  | ||||
|   hasNext() { | ||||
|   | ||||
| @@ -25,14 +25,14 @@ | ||||
|           </h5> | ||||
|         </div> | ||||
|         <p class="card-text"> | ||||
|           <app-result-highlight *ngIf="getDetailsAsHighlight()" class="result-content" [highlights]="getDetailsAsHighlight()"></app-result-highlight> | ||||
|           <span *ngIf="getDetailsAsString()" class="result-content">{{getDetailsAsString()}}</span> | ||||
|           <span *ngIf="document.__search_hit__" [innerHtml]="document.__search_hit__.highlights"></span> | ||||
|           <span *ngIf="!document.__search_hit__" class="result-content">{{contentTrimmed}}</span> | ||||
|         </p> | ||||
|  | ||||
|  | ||||
|         <div class="d-flex flex-column flex-md-row align-items-md-center"> | ||||
|           <div class="btn-group"> | ||||
|             <a routerLink="/search" [queryParams]="{'more_like': document.id}" class="btn btn-sm btn-outline-secondary" *ngIf="moreLikeThis"> | ||||
|             <a class="btn btn-sm btn-outline-secondary" (click)="clickMoreLike.emit()"> | ||||
|               <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-three-dots" viewBox="0 0 16 16"> | ||||
|                 <path fill-rule="evenodd" d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/> | ||||
|               </svg> <span class="d-block d-md-inline" i18n>More like this</span> | ||||
| @@ -62,9 +62,9 @@ | ||||
|           </div> | ||||
|  | ||||
|           <div class="list-group list-group-horizontal border-0 card-info ml-md-auto mt-2 mt-md-0"> | ||||
|             <div *ngIf="searchScore" class="list-group-item bg-light text-dark p-1 mr-5 border-0 d-flex search-score"> | ||||
|             <div *ngIf="document.__search_hit__" class="list-group-item bg-light text-dark p-1 mr-5 border-0 d-flex search-score"> | ||||
|               <small class="text-muted" i18n>Score:</small> | ||||
|               <ngb-progressbar [type]="searchScoreClass" [value]="searchScore" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar> | ||||
|               <ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar> | ||||
|             </div> | ||||
|             <button *ngIf="document.document_type" type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 mr-2" title="Filter by document type" | ||||
|              (click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()"> | ||||
|   | ||||
| @@ -60,3 +60,8 @@ | ||||
|     padding-top: 0.35rem !important; | ||||
|   } | ||||
| } | ||||
|  | ||||
| span ::ng-deep .match { | ||||
|   color: black; | ||||
|   background-color: rgb(255, 211, 66); | ||||
| } | ||||
| @@ -4,6 +4,8 @@ import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | ||||
| import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service'; | ||||
| import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service'; | ||||
| import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-document-card-large', | ||||
| @@ -24,15 +26,9 @@ export class DocumentCardLargeComponent implements OnInit { | ||||
|     return this.toggleSelected.observers.length > 0 | ||||
|   } | ||||
|  | ||||
|   @Input() | ||||
|   moreLikeThis: boolean = false | ||||
|  | ||||
|   @Input() | ||||
|   document: PaperlessDocument | ||||
|  | ||||
|   @Input() | ||||
|   details: any | ||||
|  | ||||
|   @Output() | ||||
|   clickTag = new EventEmitter<number>() | ||||
|  | ||||
| @@ -42,6 +38,9 @@ export class DocumentCardLargeComponent implements OnInit { | ||||
|   @Output() | ||||
|   clickDocumentType = new EventEmitter<number>() | ||||
|  | ||||
|   @Output() | ||||
|   clickMoreLike= new EventEmitter() | ||||
|  | ||||
|   @Input() | ||||
|   searchScore: number | ||||
|  | ||||
| @@ -67,19 +66,6 @@ export class DocumentCardLargeComponent implements OnInit { | ||||
|     return this.settingsService.get(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED) | ||||
|   } | ||||
|  | ||||
|   getDetailsAsString() { | ||||
|     if (typeof this.details === 'string') { | ||||
|       return this.details.substring(0, 500) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   getDetailsAsHighlight() { | ||||
|     //TODO: this is not an exact typecheck, can we do better | ||||
|     if (this.details instanceof Array) { | ||||
|       return this.details | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   getThumbUrl() { | ||||
|     return this.documentService.getThumbUrl(this.document.id) | ||||
|   } | ||||
| @@ -116,4 +102,8 @@ export class DocumentCardLargeComponent implements OnInit { | ||||
|   mouseLeaveCard() { | ||||
|     this.popover.close() | ||||
|   } | ||||
|  | ||||
|   get contentTrimmed() { | ||||
|     return this.document.content.substr(0, 500) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -90,7 +90,7 @@ | ||||
| </div> | ||||
|  | ||||
| <div *ngIf="displayMode == 'largeCards'"> | ||||
|   <app-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" [details]="d.content" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)"> | ||||
|   <app-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickMoreLike)="clickMoreLike(d.id)"> | ||||
|   </app-document-card-large> | ||||
| </div> | ||||
|  | ||||
|   | ||||
| @@ -207,6 +207,13 @@ export class DocumentListComponent implements OnInit, OnDestroy { | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   clickMoreLike(documentID: number) { | ||||
|     this.list.selectNone() | ||||
|     setTimeout(() => { | ||||
|       //this.filterEditor.moreLikeThis(doc) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   trackByDocumentId(index, item: PaperlessDocument) { | ||||
|     return item.id | ||||
|   } | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| <div class="row"> | ||||
|    <div class="col mb-2 mb-xl-0"> | ||||
|      <div class="form-inline d-flex align-items-center"> | ||||
|          <label class="text-muted mr-2 mb-0" i18n>Filter by:</label> | ||||
|          <div class="input-group input-group-sm flex-fill w-auto"> | ||||
|            <div class="input-group-prepend" ngbDropdown> | ||||
|             <button class="btn btn-outline-primary" ngbDropdownToggle>{{textFilterTargetName}}</button>    | ||||
| @@ -9,7 +8,8 @@ | ||||
|               <button *ngFor="let t of textFilterTargets" ngbDropdownItem [class.active]="textFilterTarget == t.id" (click)="changeTextFilterTarget(t.id)">{{t.name}}</button> | ||||
|             </div> | ||||
|           </div> | ||||
|           <input class="form-control form-control-sm" type="text" [(ngModel)]="textFilter"> | ||||
|           <input class="form-control form-control-sm" type="text" [(ngModel)]="textFilter" *ngIf="textFilterTarget != 'fulltext-morelike'"> | ||||
|           <span class="form-control form-control-sm text-truncate" *ngIf="textFilterTarget == 'fulltext-morelike'">{{_moreLikeDoc?.title}}</span> | ||||
|          </div> | ||||
|      </div> | ||||
|   </div> | ||||
|   | ||||
| @@ -8,13 +8,17 @@ import { DocumentTypeService } from 'src/app/services/rest/document-type.service | ||||
| import { TagService } from 'src/app/services/rest/tag.service'; | ||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; | ||||
| import { FilterRule } from 'src/app/data/filter-rule'; | ||||
| import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, FILTER_ASN, FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, FILTER_DOCUMENT_TYPE, FILTER_HAS_ANY_TAG, FILTER_HAS_TAG, FILTER_TITLE, FILTER_TITLE_CONTENT } from 'src/app/data/filter-rule-type'; | ||||
| import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, FILTER_ASN, FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, FILTER_DOCUMENT_TYPE, FILTER_FULLTEXT_MORELIKE, FILTER_FULLTEXT_QUERY, FILTER_HAS_ANY_TAG, FILTER_HAS_TAG, FILTER_TITLE, FILTER_TITLE_CONTENT } from 'src/app/data/filter-rule-type'; | ||||
| import { FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component'; | ||||
| import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'; | ||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||
|  | ||||
| const TEXT_FILTER_TARGET_TITLE = "title" | ||||
| const TEXT_FILTER_TARGET_TITLE_CONTENT = "title-content" | ||||
| const TEXT_FILTER_TARGET_ASN = "asn" | ||||
| const TEXT_FILTER_TARGET_FULLTEXT_QUERY = "fulltext-query" | ||||
| const TEXT_FILTER_TARGET_FULLTEXT_MORELIKE = "fulltext-morelike" | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-filter-editor', | ||||
| @@ -64,7 +68,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|   constructor( | ||||
|     private documentTypeService: DocumentTypeService, | ||||
|     private tagService: TagService, | ||||
|     private correspondentService: CorrespondentService | ||||
|     private correspondentService: CorrespondentService, | ||||
|     private documentService: DocumentService | ||||
|   ) { } | ||||
|  | ||||
|   tags: PaperlessTag[] = [] | ||||
| @@ -72,12 +77,21 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|   documentTypes: PaperlessDocumentType[] = [] | ||||
|  | ||||
|   _textFilter = "" | ||||
|   _moreLikeId: number | ||||
|   _moreLikeDoc: PaperlessDocument | ||||
|  | ||||
|   textFilterTargets = [ | ||||
|     {id: TEXT_FILTER_TARGET_TITLE, name: $localize`Title`}, | ||||
|     {id: TEXT_FILTER_TARGET_TITLE_CONTENT, name: $localize`Title & content`}, | ||||
|     {id: TEXT_FILTER_TARGET_ASN, name: $localize`ASN`} | ||||
|   ] | ||||
|   get textFilterTargets() { | ||||
|     let targets = [ | ||||
|       {id: TEXT_FILTER_TARGET_TITLE, name: $localize`Title`}, | ||||
|       {id: TEXT_FILTER_TARGET_TITLE_CONTENT, name: $localize`Title & content`}, | ||||
|       {id: TEXT_FILTER_TARGET_ASN, name: $localize`ASN`}, | ||||
|       {id: TEXT_FILTER_TARGET_FULLTEXT_QUERY, name: $localize`Fulltext search`} | ||||
|     ] | ||||
|     if (this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE) { | ||||
|       targets.push({id: TEXT_FILTER_TARGET_FULLTEXT_MORELIKE, name: $localize`More like`}) | ||||
|     } | ||||
|     return targets | ||||
|   } | ||||
|  | ||||
|   textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT | ||||
|  | ||||
| @@ -101,6 +115,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|     this.tagSelectionModel.clear(false) | ||||
|     this.correspondentSelectionModel.clear(false) | ||||
|     this._textFilter = null | ||||
|     this._moreLikeId = null | ||||
|     this.dateAddedBefore = null | ||||
|     this.dateAddedAfter = null | ||||
|     this.dateCreatedBefore = null | ||||
| @@ -120,6 +135,17 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|           this._textFilter = rule.value | ||||
|           this.textFilterTarget = TEXT_FILTER_TARGET_ASN | ||||
|           break | ||||
|         case FILTER_FULLTEXT_QUERY: | ||||
|           this._textFilter = rule.value | ||||
|           this.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_QUERY | ||||
|           break | ||||
|         case FILTER_FULLTEXT_MORELIKE: | ||||
|           this._moreLikeId = +rule.value | ||||
|           this.textFilterTarget = TEXT_FILTER_TARGET_FULLTEXT_MORELIKE | ||||
|           this.documentService.get(this._moreLikeId).subscribe(result => { | ||||
|             this._moreLikeDoc = result | ||||
|           }) | ||||
|           break | ||||
|         case FILTER_CREATED_AFTER: | ||||
|           this.dateCreatedAfter = rule.value | ||||
|           break | ||||
| @@ -159,6 +185,12 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|     if (this._textFilter && this.textFilterTarget == TEXT_FILTER_TARGET_ASN) { | ||||
|       filterRules.push({rule_type: FILTER_ASN, value: this._textFilter}) | ||||
|     } | ||||
|     if (this._textFilter && this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_QUERY) { | ||||
|       filterRules.push({rule_type: FILTER_FULLTEXT_QUERY, value: this._textFilter}) | ||||
|     } | ||||
|     if (this._moreLikeId && this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE) { | ||||
|       filterRules.push({rule_type: FILTER_FULLTEXT_MORELIKE, value: this._moreLikeId?.toString()}) | ||||
|     } | ||||
|     if (this.tagSelectionModel.isNoneSelected()) { | ||||
|       filterRules.push({rule_type: FILTER_HAS_ANY_TAG, value: "false"}) | ||||
|     } else { | ||||
| @@ -232,6 +264,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|   } | ||||
|  | ||||
|   resetSelected() { | ||||
|     this.textFilterTarget = TEXT_FILTER_TARGET_TITLE_CONTENT | ||||
|     this.reset.next() | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| ... <span *ngFor="let fragment of highlights"> | ||||
|     <span *ngFor="let token of fragment" [class.match]="token.highlight">{{token.text}}</span> ...  | ||||
| </span> | ||||
| @@ -1,4 +0,0 @@ | ||||
| .match { | ||||
|     color: black; | ||||
|     background-color: rgb(255, 211, 66); | ||||
| } | ||||
| @@ -1,25 +0,0 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { ResultHighlightComponent } from './result-highlight.component'; | ||||
|  | ||||
| describe('ResultHighlightComponent', () => { | ||||
|   let component: ResultHighlightComponent; | ||||
|   let fixture: ComponentFixture<ResultHighlightComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ ResultHighlightComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(ResultHighlightComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,19 +0,0 @@ | ||||
| import { Component, Input, OnInit } from '@angular/core'; | ||||
| import { SearchHitHighlight } from 'src/app/data/search-result'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-result-highlight', | ||||
|   templateUrl: './result-highlight.component.html', | ||||
|   styleUrls: ['./result-highlight.component.scss'] | ||||
| }) | ||||
| export class ResultHighlightComponent implements OnInit { | ||||
|  | ||||
|   constructor() { } | ||||
|  | ||||
|   @Input() | ||||
|   highlights: SearchHitHighlight[][] | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -1,26 +0,0 @@ | ||||
| <app-page-header i18n-title title="Search results"> | ||||
| </app-page-header> | ||||
|  | ||||
| <div *ngIf="errorMessage" class="alert alert-danger" i18n>Invalid search query: {{errorMessage}}</div> | ||||
|  | ||||
| <p *ngIf="more_like" i18n>Showing documents similar to <a routerLink="/documents/{{more_like}}">{{more_like_doc?.original_file_name}}</a></p> | ||||
|  | ||||
| <p *ngIf="query"> | ||||
|     <ng-container i18n>Search query: <i>{{query}}</i></ng-container> | ||||
|     <ng-container *ngIf="correctedQuery"> | ||||
|         - <ng-container i18n>Did you mean "<a [routerLink]="" (click)="searchCorrectedQuery()">{{correctedQuery}}</a>"?</ng-container> | ||||
|     </ng-container> | ||||
| </p> | ||||
|  | ||||
| <div *ngIf="!errorMessage" [class.result-content-searching]="searching" infiniteScroll (scrolled)="onScroll()"> | ||||
|     <p i18n>{resultCount, plural, =0 {No results} =1 {One result} other {{{resultCount}} results}}</p> | ||||
|     <ng-container *ngFor="let result of results"> | ||||
|         <app-document-card-large *ngIf="result.document" | ||||
|             [document]="result.document" | ||||
|             [details]="result.highlights" | ||||
|             [searchScore]="result.score / maxScore" | ||||
|             [moreLikeThis]="true"> | ||||
|         </app-document-card-large> | ||||
|     </ng-container> | ||||
|  | ||||
| </div> | ||||
| @@ -1,15 +0,0 @@ | ||||
| .result-content { | ||||
|     color: darkgray; | ||||
| } | ||||
|  | ||||
| .doc-img { | ||||
|     object-fit: cover; | ||||
|     object-position: top; | ||||
|     height: 100%; | ||||
|     position: absolute; | ||||
|  | ||||
| } | ||||
|  | ||||
| .result-content-searching { | ||||
|     opacity: 0.3; | ||||
| } | ||||
| @@ -1,25 +0,0 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
|  | ||||
| import { SearchComponent } from './search.component'; | ||||
|  | ||||
| describe('SearchComponent', () => { | ||||
|   let component: SearchComponent; | ||||
|   let fixture: ComponentFixture<SearchComponent>; | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ SearchComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(SearchComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
|  | ||||
|   it('should create', () => { | ||||
|     expect(component).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,95 +0,0 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { ActivatedRoute, Router } from '@angular/router'; | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; | ||||
| import { SearchHit } from 'src/app/data/search-result'; | ||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | ||||
| import { SearchService } from 'src/app/services/rest/search.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-search', | ||||
|   templateUrl: './search.component.html', | ||||
|   styleUrls: ['./search.component.scss'] | ||||
| }) | ||||
| export class SearchComponent implements OnInit { | ||||
|  | ||||
|   results: SearchHit[] = [] | ||||
|  | ||||
|   query: string = "" | ||||
|  | ||||
|   more_like: number | ||||
|  | ||||
|   more_like_doc: PaperlessDocument | ||||
|  | ||||
|   searching = false | ||||
|  | ||||
|   currentPage = 1 | ||||
|  | ||||
|   pageCount = 1 | ||||
|  | ||||
|   resultCount | ||||
|  | ||||
|   correctedQuery: string = null | ||||
|  | ||||
|   errorMessage: string | ||||
|  | ||||
|   get maxScore() { | ||||
|     return this.results?.length > 0 ? this.results[0].score : 100 | ||||
|   } | ||||
|  | ||||
|   constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router, private documentService: DocumentService) { } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     this.route.queryParamMap.subscribe(paramMap => { | ||||
|       window.scrollTo(0, 0) | ||||
|       this.query = paramMap.get('query') | ||||
|       this.more_like = paramMap.has('more_like') ? +paramMap.get('more_like') : null | ||||
|       if (this.more_like) { | ||||
|         this.documentService.get(this.more_like).subscribe(r => { | ||||
|           this.more_like_doc = r | ||||
|         }) | ||||
|       } else { | ||||
|         this.more_like_doc = null | ||||
|       } | ||||
|       this.searching = true | ||||
|       this.currentPage = 1 | ||||
|       this.loadPage() | ||||
|     }) | ||||
|  | ||||
|   } | ||||
|  | ||||
|   searchCorrectedQuery() { | ||||
|     this.router.navigate(["search"], {queryParams: {query: this.correctedQuery, more_like: this.more_like}}) | ||||
|   } | ||||
|  | ||||
|   loadPage(append: boolean = false) { | ||||
|     this.errorMessage = null | ||||
|     this.correctedQuery = null | ||||
|  | ||||
|     this.searchService.search(this.query, this.currentPage, this.more_like).subscribe(result => { | ||||
|       if (append) { | ||||
|         this.results.push(...result.results) | ||||
|       } else { | ||||
|         this.results = result.results | ||||
|       } | ||||
|       this.pageCount = result.page_count | ||||
|       this.searching = false | ||||
|       this.resultCount = result.count | ||||
|       this.correctedQuery = result.corrected_query | ||||
|     }, error => { | ||||
|       this.searching = false | ||||
|       this.resultCount = 1 | ||||
|       this.pageCount = 1 | ||||
|       this.results = [] | ||||
|       this.errorMessage = error.error | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   onScroll() { | ||||
|     if (this.currentPage < this.pageCount) { | ||||
|       this.currentPage += 1 | ||||
|       this.loadPage(true) | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -22,6 +22,9 @@ export const FILTER_ASN_ISNULL = 18 | ||||
|  | ||||
| export const FILTER_TITLE_CONTENT = 19 | ||||
|  | ||||
| export const FILTER_FULLTEXT_QUERY = 20 | ||||
| export const FILTER_FULLTEXT_MORELIKE = 21 | ||||
|  | ||||
| export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|  | ||||
|   {id: FILTER_TITLE, filtervar: "title__icontains", datatype: "string", multi: false, default: ""}, | ||||
| @@ -51,7 +54,11 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|   {id: FILTER_MODIFIED_AFTER, filtervar: "modified__date__gt", datatype: "date", multi: false}, | ||||
|   {id: FILTER_ASN_ISNULL, filtervar: "archive_serial_number__isnull", datatype: "boolean", multi: false}, | ||||
|  | ||||
|   {id: FILTER_TITLE_CONTENT, filtervar: "title_content", datatype: "string", multi: false} | ||||
|   {id: FILTER_TITLE_CONTENT, filtervar: "title_content", datatype: "string", multi: false}, | ||||
|  | ||||
|   {id: FILTER_FULLTEXT_QUERY, filtervar: "query", datatype: "string", multi: false}, | ||||
|  | ||||
|   {id: FILTER_FULLTEXT_MORELIKE, filtervar: "more_like_id", datatype: "number", multi: false}, | ||||
| ] | ||||
|  | ||||
| export interface FilterRuleType { | ||||
|   | ||||
| @@ -4,6 +4,15 @@ import { PaperlessTag } from './paperless-tag' | ||||
| import { PaperlessDocumentType } from './paperless-document-type' | ||||
| import { Observable } from 'rxjs' | ||||
|  | ||||
| export interface SearchHit { | ||||
|  | ||||
|   score?: number | ||||
|   rank?: number | ||||
|  | ||||
|   highlights?: string | ||||
|  | ||||
| } | ||||
|  | ||||
| export interface PaperlessDocument extends ObjectWithId { | ||||
|  | ||||
|     correspondent$?: Observable<PaperlessCorrespondent> | ||||
| @@ -40,4 +49,6 @@ export interface PaperlessDocument extends ObjectWithId { | ||||
|  | ||||
|     archive_serial_number?: number | ||||
|  | ||||
|     __search_hit__?: SearchHit | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,29 +0,0 @@ | ||||
| import { PaperlessDocument } from './paperless-document' | ||||
|  | ||||
| export class SearchHitHighlight { | ||||
|   text?: string | ||||
|   term?: number | ||||
| } | ||||
|  | ||||
| export interface SearchHit { | ||||
|   id?: number | ||||
|   title?: string | ||||
|   score?: number | ||||
|   rank?: number | ||||
|  | ||||
|   highlights?: SearchHitHighlight[][] | ||||
|   document?: PaperlessDocument | ||||
| } | ||||
|  | ||||
| export interface SearchResult { | ||||
|  | ||||
|   count?: number | ||||
|   page?: number | ||||
|   page_count?: number | ||||
|  | ||||
|   corrected_query?: string | ||||
|  | ||||
|   results?: SearchHit[] | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -1,7 +1,9 @@ | ||||
| import { Route } from '@angular/compiler/src/core'; | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { Router } from '@angular/router'; | ||||
| import { ActivatedRoute, Router } from '@angular/router'; | ||||
| import { Observable } from 'rxjs'; | ||||
| import { cloneFilterRules, FilterRule } from '../data/filter-rule'; | ||||
| import { FILTER_FULLTEXT_MORELIKE, FILTER_FULLTEXT_QUERY } from '../data/filter-rule-type'; | ||||
| import { PaperlessDocument } from '../data/paperless-document'; | ||||
| import { PaperlessSavedView } from '../data/paperless-saved-view'; | ||||
| import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'; | ||||
| @@ -207,7 +209,11 @@ export class DocumentListViewService { | ||||
|     this.activeListViewState.currentPage = 1 | ||||
|     this.reduceSelectionToFilter() | ||||
|     this.saveDocumentListView() | ||||
|     this.router.navigate(["documents"]) | ||||
|     if (this.router.url == "/documents") { | ||||
|       this.reload() | ||||
|     } else { | ||||
|       this.router.navigate(["documents"]) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   getLastPage(): number { | ||||
| @@ -317,7 +323,7 @@ export class DocumentListViewService { | ||||
|     return this.documents.map(d => d.id).indexOf(documentID) | ||||
|   } | ||||
|  | ||||
|   constructor(private documentService: DocumentService, private settings: SettingsService, private router: Router) { | ||||
|   constructor(private documentService: DocumentService, private settings: SettingsService, private router: Router, private route: ActivatedRoute) { | ||||
|      let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG) | ||||
|     if (documentListViewConfigJson) { | ||||
|       try { | ||||
|   | ||||
| @@ -2,8 +2,6 @@ import { HttpClient, HttpParams } from '@angular/common/http'; | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { Observable } from 'rxjs'; | ||||
| import { map } from 'rxjs/operators'; | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||
| import { SearchResult } from 'src/app/data/search-result'; | ||||
| import { environment } from 'src/environments/environment'; | ||||
| import { DocumentService } from './document.service'; | ||||
|  | ||||
| @@ -13,30 +11,7 @@ import { DocumentService } from './document.service'; | ||||
| }) | ||||
| export class SearchService { | ||||
|    | ||||
|   constructor(private http: HttpClient, private documentService: DocumentService) { } | ||||
|  | ||||
|   search(query: string, page?: number, more_like?: number): Observable<SearchResult> { | ||||
|     let httpParams = new HttpParams() | ||||
|     if (query) { | ||||
|       httpParams = httpParams.set('query', query) | ||||
|     } | ||||
|     if (page) { | ||||
|       httpParams = httpParams.set('page', page.toString()) | ||||
|     } | ||||
|     if (more_like) { | ||||
|       httpParams = httpParams.set('more_like', more_like.toString()) | ||||
|     } | ||||
|     return this.http.get<SearchResult>(`${environment.apiBaseUrl}search/`, {params: httpParams}).pipe( | ||||
|       map(result => { | ||||
|         result.results.forEach(hit => { | ||||
|           if (hit.document) { | ||||
|             this.documentService.addObservablesToDocument(hit.document) | ||||
|           } | ||||
|         }) | ||||
|         return result | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
|   constructor(private http: HttpClient) { } | ||||
|  | ||||
|   autocomplete(term: string): Observable<string[]> { | ||||
|     return this.http.get<string[]>(`${environment.apiBaseUrl}search/autocomplete/`, {params: new HttpParams().set('term', term)}) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 jonaswinkler
					jonaswinkler