mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge branch 'dev' into feature-ocrmypdf
This commit is contained in:
		| @@ -1,2 +1,2 @@ | ||||
| <span *ngIf="!clickable" class="badge" [style.background]="getColour().value" [style.color]="getColour().textColor">{{tag.name}}</span> | ||||
| <a [routerLink]="" *ngIf="clickable" class="badge" [style.background]="getColour().value" [style.color]="getColour().textColor">{{tag.name}}</a> | ||||
| <a [routerLink]="" [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="getColour().value" [style.color]="getColour().textColor">{{tag.name}}</a> | ||||
| @@ -14,10 +14,10 @@ export class TagComponent implements OnInit { | ||||
|   tag: PaperlessTag | ||||
|  | ||||
|   @Input() | ||||
|   clickable: boolean = false | ||||
|   linkTitle: string = "" | ||||
|  | ||||
|   @Output() | ||||
|   click = new EventEmitter() | ||||
|   @Input() | ||||
|   clickable: boolean = false | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import { DatePipe, formatDate } from '@angular/common'; | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { FormControl, FormGroup } from '@angular/forms'; | ||||
| import { ActivatedRoute, Router } from '@angular/router'; | ||||
| @@ -7,17 +6,14 @@ import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||
| import { PaperlessDocumentMetadata } from 'src/app/data/paperless-document-metadata'; | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; | ||||
| import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag'; | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service'; | ||||
| import { OpenDocumentsService } from 'src/app/services/open-documents.service'; | ||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; | ||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; | ||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | ||||
| import { TagService } from 'src/app/services/rest/tag.service'; | ||||
| import { DeleteDialogComponent } from '../common/delete-dialog/delete-dialog.component'; | ||||
| import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component'; | ||||
| import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component'; | ||||
| import { TagEditDialogComponent } from '../manage/tag-list/tag-edit-dialog/tag-edit-dialog.component'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-document-detail', | ||||
| @@ -140,8 +136,8 @@ export class DocumentDetailComponent implements OnInit { | ||||
|  | ||||
|   close() { | ||||
|     this.openDocumentService.closeDocument(this.document) | ||||
|     if (this.documentListViewService.viewId) { | ||||
|       this.router.navigate(['view', this.documentListViewService.viewId]) | ||||
|     if (this.documentListViewService.savedViewId) { | ||||
|       this.router.navigate(['view', this.documentListViewService.savedViewId]) | ||||
|     } else { | ||||
|       this.router.navigate(['documents']) | ||||
|     } | ||||
|   | ||||
| @@ -7,7 +7,12 @@ | ||||
|       <div class="card-body"> | ||||
|  | ||||
|         <div class="d-flex justify-content-between align-items-center"> | ||||
|           <h5 class="card-title">{{document.correspondent ? document.correspondent.name + ': ' : ''}}{{document.title}}<app-tag [tag]="t" *ngFor="let t of document.tags" class="ml-1"></app-tag></h5> | ||||
|           <h5 class="card-title">     | ||||
|             <ng-container *ngIf="document.correspondent"> | ||||
|               <a [routerLink]="" title="Filter by correspondent" (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{document.correspondent.name}}</a>: | ||||
|             </ng-container> | ||||
|             {{document.title}}<app-tag [tag]="t" linkTitle="Filter by tag" *ngFor="let t of document.tags" class="ml-1" (click)="clickTag.emit(t)" [clickable]="true"></app-tag> | ||||
|           </h5> | ||||
|           <h5 class="card-title" *ngIf="document.archive_serial_number">#{{document.archive_serial_number}}</h5> | ||||
|         </div> | ||||
|         <p class="card-text"> | ||||
| @@ -24,6 +29,13 @@ | ||||
|               </svg> | ||||
|               Edit | ||||
|             </a> | ||||
|             <a type="button" class="btn btn-sm btn-outline-secondary" [href]="getPreviewUrl()"> | ||||
|               <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|                 <path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/> | ||||
|                 <path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/> | ||||
|               </svg> | ||||
|               View | ||||
|             </a> | ||||
|             <a type="button" class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()"> | ||||
|               <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|                 <path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/> | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { Component, Input, OnInit } from '@angular/core'; | ||||
| import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; | ||||
| import { DomSanitizer } from '@angular/platform-browser'; | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||
| import { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | ||||
|  | ||||
| @Component({ | ||||
| @@ -18,6 +19,12 @@ export class DocumentCardLargeComponent implements OnInit { | ||||
|   @Input() | ||||
|   details: any | ||||
|  | ||||
|   @Output() | ||||
|   clickTag = new EventEmitter<PaperlessTag>() | ||||
|  | ||||
|   @Output() | ||||
|   clickCorrespondent = new EventEmitter<PaperlessDocument>() | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
| @@ -41,4 +48,8 @@ export class DocumentCardLargeComponent implements OnInit { | ||||
|   getDownloadUrl() { | ||||
|     return this.documentService.getDownloadUrl(this.document.id) | ||||
|   } | ||||
|  | ||||
|   getPreviewUrl() { | ||||
|     return this.documentService.getPreviewUrl(this.document.id) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -2,26 +2,34 @@ | ||||
|   <div class="card h-100 shadow-sm"> | ||||
|     <div class=" border-bottom doc-img pr-1" [ngStyle]="{'background-image': 'url(' + getThumbUrl() + ')'}"> | ||||
|       <div class="row" *ngFor="let t of document.tags"> | ||||
|         <app-tag [tag]="t" class="col text-right"></app-tag> | ||||
|         <app-tag style="font-size: large;" [tag]="t" class="col text-right" (click)="clickTag.emit(t)" [clickable]="true" linkTitle="Filter by tag"></app-tag> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|      | ||||
|     <div class="card-body p-2"> | ||||
|       <p class="card-text"> | ||||
|         <span class="font-weight-bold">{{document.correspondent? document.correspondent.name + ': ' : ''}}</span> {{document.title}} | ||||
|         <ng-container *ngIf="document.correspondent"> | ||||
|           <a [routerLink]="" title="Filter by correspondent" (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{document.correspondent.name}}</a>: | ||||
|         </ng-container> | ||||
|         {{document.title}} | ||||
|       </p> | ||||
|     </div> | ||||
|     <div class="card-footer"> | ||||
|  | ||||
|       <div class="d-flex justify-content-between align-items-center ml-n2"> | ||||
|         <div class="btn-group"> | ||||
|           <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary"> | ||||
|           <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit"> | ||||
|             <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|               <path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/> | ||||
|             </svg> | ||||
|           </a> | ||||
|           <a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary"> | ||||
|           <a [href]="getPreviewUrl()" class="btn btn-sm btn-outline-secondary" title="View in browser"> | ||||
|             <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|               <path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/> | ||||
|               <path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/> | ||||
|             </svg> | ||||
|           </a> | ||||
|           <a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download"> | ||||
|             <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|               <path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/> | ||||
|               <path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/> | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Component, Input, OnInit } from '@angular/core'; | ||||
| import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||
| import { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | ||||
|  | ||||
| @Component({ | ||||
| @@ -14,6 +15,12 @@ export class DocumentCardSmallComponent implements OnInit { | ||||
|   @Input() | ||||
|   document: PaperlessDocument | ||||
|  | ||||
|   @Output() | ||||
|   clickTag = new EventEmitter<PaperlessTag>() | ||||
|  | ||||
|   @Output() | ||||
|   clickCorrespondent = new EventEmitter<PaperlessDocument>() | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
| @@ -24,4 +31,8 @@ export class DocumentCardSmallComponent implements OnInit { | ||||
|   getDownloadUrl() { | ||||
|     return this.documentService.getDownloadUrl(this.document.id) | ||||
|   } | ||||
|  | ||||
|   getPreviewUrl() { | ||||
|     return this.documentService.getPreviewUrl(this.document.id) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -21,13 +21,12 @@ | ||||
|       </svg> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="docs.sortDirection" | ||||
|     *ngIf="!docs.viewId"> | ||||
|   <div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortDirection"> | ||||
|     <div ngbDropdown class="btn-group"> | ||||
|       <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>Sort by</button> | ||||
|       <div ngbDropdownMenu aria-labelledby="dropdownBasic1"> | ||||
|         <button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="setSort(f.field)" | ||||
|           [class.active]="docs.sortField == f.field">{{f.name}}</button> | ||||
|         <button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="list.sortField = f.field" | ||||
|           [class.active]="list.sortField == f.field">{{f.name}}</button> | ||||
|       </div> | ||||
|     </div> | ||||
|     <label ngbButtonLabel class="btn-outline-primary btn-sm"> | ||||
| @@ -43,7 +42,7 @@ | ||||
|       </svg> | ||||
|     </label> | ||||
|   </div> | ||||
|   <div class="btn-group ml-2" *ngIf="!docs.viewId"> | ||||
|   <div class="btn-group ml-2"> | ||||
|  | ||||
|     <button type="button" class="btn btn-sm btn-outline-primary" (click)="showFilter=!showFilter"> | ||||
|       <svg class="toolbaricon" fill="currentColor"> | ||||
| @@ -55,9 +54,13 @@ | ||||
|     <div class="btn-group" ngbDropdown role="group"> | ||||
|       <button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button> | ||||
|       <div class="dropdown-menu" ngbDropdownMenu> | ||||
|         <button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button> | ||||
|         <div class="dropdown-divider" *ngIf="savedViewConfigService.getConfigs().length > 0"></div> | ||||
|         <button ngbDropdownItem (click)="saveViewConfig()">Save current view</button> | ||||
|         <ng-container *ngIf="!list.savedViewId" > | ||||
|           <button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button> | ||||
|           <div class="dropdown-divider" *ngIf="savedViewConfigService.getConfigs().length > 0"></div> | ||||
|         </ng-container> | ||||
|          | ||||
|         <button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId">Save "{{list.savedViewTitle}}"</button> | ||||
|         <button ngbDropdownItem (click)="saveViewConfigAs()">Save as...</button> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
| @@ -72,16 +75,16 @@ | ||||
| </div> | ||||
|  | ||||
| <div class="row m-0 justify-content-end"> | ||||
|   <ngb-pagination [pageSize]="docs.currentPageSize" [collectionSize]="docs.collectionSize" [(page)]="docs.currentPage" [maxSize]="5" | ||||
|   [rotate]="true" (pageChange)="reload()" aria-label="Default pagination"></ngb-pagination> | ||||
|   <ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5" | ||||
|   [rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination> | ||||
| </div> | ||||
|  | ||||
| <div *ngIf="displayMode == 'largeCards'"> | ||||
|   <app-document-card-large *ngFor="let d of docs.documents" [document]="d" [details]="d.content"> | ||||
|   <app-document-card-large *ngFor="let d of list.documents" [document]="d" [details]="d.content" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)"> | ||||
|   </app-document-card-large> | ||||
| </div> | ||||
|  | ||||
| <table class="table table-hover table-sm border shadow" *ngIf="displayMode == 'details'"> | ||||
| <table class="table table-sm border shadow" *ngIf="displayMode == 'details'"> | ||||
|   <thead> | ||||
|     <th class="d-none d-lg-table-cell">ASN</th> | ||||
|     <th class="d-none d-md-table-cell">Correspondent</th> | ||||
| @@ -91,20 +94,37 @@ | ||||
|     <th class="d-none d-xl-table-cell">Added</th> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr *ngFor="let d of docs.documents" routerLink="/documents/{{d.id}}"> | ||||
|       <td class="d-none d-lg-table-cell">{{d.archive_serial_number}}</td> | ||||
|       <td class="d-none d-md-table-cell">{{d.correspondent ? d.correspondent.name : ''}}</td> | ||||
|       <td>{{d.title}}<app-tag [tag]="t" *ngFor="let t of d.tags" class="ml-1"></app-tag></td> | ||||
|       <td class="d-none d-xl-table-cell">{{d.document_type ? d.document_type.name : ''}}</td> | ||||
|       <td>{{d.created | date}}</td> | ||||
|       <td class="d-none d-xl-table-cell">{{d.added | date}}</td> | ||||
|     <tr *ngFor="let d of list.documents"> | ||||
|       <td class="d-none d-lg-table-cell"> | ||||
|         {{d.archive_serial_number}} | ||||
|       </td> | ||||
|       <td class="d-none d-md-table-cell"> | ||||
|         <ng-container *ngIf="d.correspondent"> | ||||
|           <a [routerLink]="" (click)="filterByCorrespondent(d.correspondent)" title="Filter by correspondent">{{d.correspondent.name}}</a> | ||||
|         </ng-container> | ||||
|       </td> | ||||
|       <td> | ||||
|         <a routerLink="/documents/{{d.id}}" title="Edit document">{{d.title}}</a> | ||||
|         <app-tag [tag]="t" *ngFor="let t of d.tags" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="filterByTag(t)"></app-tag> | ||||
|       </td> | ||||
|       <td class="d-none d-xl-table-cell"> | ||||
|         <ng-container *ngIf="d.document_type"> | ||||
|           <a [routerLink]="" (click)="filterByDocumentType(d.document_type)" title="Filter by document type">{{d.document_type.name}}</a> | ||||
|         </ng-container> | ||||
|       </td> | ||||
|       <td> | ||||
|         {{d.created | date}} | ||||
|       </td> | ||||
|       <td class="d-none d-xl-table-cell"> | ||||
|         {{d.added | date}} | ||||
|       </td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
|  | ||||
| <div class=" m-n2 row" *ngIf="displayMode == 'smallCards'"> | ||||
|   <app-document-card-small [document]="d" *ngFor="let d of docs.documents"></app-document-card-small>     | ||||
|   <app-document-card-small [document]="d" *ngFor="let d of list.documents" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)"></app-document-card-small>     | ||||
| </div> | ||||
|  | ||||
| <p *ngIf="docs.documents.length == 0" class="mx-auto">No results</p> | ||||
| <p *ngIf="list.documents.length == 0" class="mx-auto">No results</p> | ||||
|   | ||||
| @@ -1,11 +1,16 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { ActivatedRoute, Router } from '@angular/router'; | ||||
| import { ActivatedRoute } from '@angular/router'; | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { cloneFilterRules, FilterRule } from 'src/app/data/filter-rule'; | ||||
| import { FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; | ||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; | ||||
| import { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||
| import { SavedViewConfig } from 'src/app/data/saved-view-config'; | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service'; | ||||
| import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service'; | ||||
| import { SavedViewConfigService } from 'src/app/services/saved-view-config.service'; | ||||
| import { Toast, ToastService } from 'src/app/services/toast.service'; | ||||
| import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'; | ||||
|  | ||||
| @Component({ | ||||
| @@ -16,9 +21,10 @@ import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-vi | ||||
| export class DocumentListComponent implements OnInit { | ||||
|  | ||||
|   constructor( | ||||
|     public docs: DocumentListViewService, | ||||
|     public list: DocumentListViewService, | ||||
|     public savedViewConfigService: SavedViewConfigService, | ||||
|     public route: ActivatedRoute, | ||||
|     private toastService: ToastService, | ||||
|     public modalService: NgbModal) { } | ||||
|  | ||||
|   displayMode = 'smallCards' // largeCards, smallCards, details | ||||
| @@ -27,17 +33,13 @@ export class DocumentListComponent implements OnInit { | ||||
|   showFilter = false | ||||
|  | ||||
|   getTitle() { | ||||
|     return this.docs.viewConfigOverride ? this.docs.viewConfigOverride.title : "Documents" | ||||
|     return this.list.savedViewTitle || "Documents" | ||||
|   } | ||||
|  | ||||
|   getSortFields() { | ||||
|     return DOCUMENT_SORT_FIELDS | ||||
|   } | ||||
|  | ||||
|   setSort(field: string) { | ||||
|     this.docs.sortField = field | ||||
|   } | ||||
|  | ||||
|   saveDisplayMode() { | ||||
|     localStorage.setItem('document-list:displayMode', this.displayMode) | ||||
|   } | ||||
| @@ -48,41 +50,74 @@ export class DocumentListComponent implements OnInit { | ||||
|     } | ||||
|     this.route.paramMap.subscribe(params => { | ||||
|       if (params.has('id')) { | ||||
|         this.docs.viewConfigOverride = this.savedViewConfigService.getConfig(params.get('id')) | ||||
|         this.list.savedView = this.savedViewConfigService.getConfig(params.get('id')) | ||||
|       } else { | ||||
|         this.filterRules = this.docs.filterRules | ||||
|         this.showFilter = this.filterRules.length > 0 | ||||
|         this.docs.viewConfigOverride = null | ||||
|         this.list.savedView = null | ||||
|       } | ||||
|       this.reload() | ||||
|       this.filterRules = this.list.filterRules | ||||
|       //this.showFilter = this.filterRules.length > 0 | ||||
|       // prevents temporarily visible results from previous views | ||||
|       this.list.documents = [] | ||||
|       this.list.reload() | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   reload() { | ||||
|     this.docs.reload() | ||||
|   } | ||||
|  | ||||
|   applyFilterRules() { | ||||
|     this.docs.filterRules = this.filterRules | ||||
|     this.list.filterRules = this.filterRules | ||||
|   } | ||||
|  | ||||
|   loadViewConfig(config: SavedViewConfig) { | ||||
|     this.filterRules = cloneFilterRules(config.filterRules) | ||||
|     this.docs.loadViewConfig(config) | ||||
|     this.list.load(config) | ||||
|   } | ||||
|  | ||||
|   saveViewConfig() { | ||||
|     this.savedViewConfigService.updateConfig(this.list.savedView) | ||||
|     this.toastService.showToast(Toast.make("Information", `View "${this.list.savedView.title}" saved successfully.`)) | ||||
|   } | ||||
|  | ||||
|   saveViewConfigAs() { | ||||
|     let modal = this.modalService.open(SaveViewConfigDialogComponent, {backdrop: 'static'}) | ||||
|     modal.componentInstance.saveClicked.subscribe(formValue => { | ||||
|       this.savedViewConfigService.saveConfig({ | ||||
|       this.savedViewConfigService.newConfig({ | ||||
|         title: formValue.title, | ||||
|         showInDashboard: formValue.showInDashboard, | ||||
|         showInSideBar: formValue.showInSideBar, | ||||
|         filterRules: this.docs.filterRules, | ||||
|         sortDirection: this.docs.sortDirection, | ||||
|         sortField: this.docs.sortField | ||||
|         filterRules: this.list.filterRules, | ||||
|         sortDirection: this.list.sortDirection, | ||||
|         sortField: this.list.sortField | ||||
|       }) | ||||
|       modal.close() | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   filterByTag(t: PaperlessTag) { | ||||
|     if (this.filterRules.find(rule => rule.type.id == FILTER_HAS_TAG && rule.value == t.id)) { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     this.filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_HAS_TAG), value: t.id}) | ||||
|     this.applyFilterRules() | ||||
|   } | ||||
|  | ||||
|   filterByCorrespondent(c: PaperlessCorrespondent) { | ||||
|     let existing_rule = this.filterRules.find(rule => rule.type.id == FILTER_CORRESPONDENT) | ||||
|     if (existing_rule) { | ||||
|       existing_rule.value = c.id | ||||
|     } else { | ||||
|       this.filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_CORRESPONDENT), value: c.id}) | ||||
|     } | ||||
|     this.applyFilterRules() | ||||
|   } | ||||
|  | ||||
|   filterByDocumentType(dt: PaperlessDocumentType) { | ||||
|     let existing_rule = this.filterRules.find(rule => rule.type.id == FILTER_DOCUMENT_TYPE) | ||||
|     if (existing_rule) { | ||||
|       existing_rule.value = dt.id | ||||
|     } else { | ||||
|       this.filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_DOCUMENT_TYPE), value: dt.id}) | ||||
|     } | ||||
|     this.applyFilterRules() | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -11,5 +11,5 @@ | ||||
| } | ||||
|  | ||||
| .result-content-searching { | ||||
|     opacity: 0.2; | ||||
|     opacity: 0.3; | ||||
| } | ||||
| @@ -1,31 +1,51 @@ | ||||
| export const FILTER_TITLE = 0 | ||||
| export const FILTER_CONTENT = 1 | ||||
| export const FILTER_ASN = 2 | ||||
| export const FILTER_CORRESPONDENT = 3 | ||||
| export const FILTER_DOCUMENT_TYPE = 4 | ||||
| export const FILTER_IS_IN_INBOX = 5 | ||||
| export const FILTER_HAS_TAG = 6 | ||||
| export const FILTER_HAS_ANY_TAG = 7 | ||||
| export const FILTER_CREATED_BEFORE = 8 | ||||
| export const FILTER_CREATED_AFTER = 9 | ||||
| export const FILTER_CREATED_YEAR = 10 | ||||
| export const FILTER_CREATED_MONTH = 11 | ||||
| export const FILTER_CREATED_DAY = 12 | ||||
| export const FILTER_ADDED_BEFORE = 13 | ||||
| export const FILTER_ADDED_AFTER = 14 | ||||
| export const FILTER_MODIFIED_BEFORE = 15 | ||||
| export const FILTER_MODIFIED_AFTER = 16 | ||||
|  | ||||
| export const FILTER_RULE_TYPES: FilterRuleType[] = [ | ||||
|   {name: "Title contains", filtervar: "title__icontains", datatype: "string", multi: false}, | ||||
|   {name: "Content contains", filtervar: "content__icontains", datatype: "string", multi: false}, | ||||
|  | ||||
|   {id: FILTER_TITLE, name: "Title contains", filtervar: "title__icontains", datatype: "string", multi: false}, | ||||
|   {id: FILTER_CONTENT, name: "Content contains", filtervar: "content__icontains", datatype: "string", multi: false}, | ||||
|    | ||||
|   {name: "ASN is", filtervar: "archive_serial_number", datatype: "number", multi: false}, | ||||
|   {id: FILTER_ASN, name: "ASN is", filtervar: "archive_serial_number", datatype: "number", multi: false}, | ||||
|    | ||||
|   {name: "Correspondent is", filtervar: "correspondent__id", datatype: "correspondent", multi: false}, | ||||
|   {name: "Document type is", filtervar: "document_type__id", datatype: "document_type", multi: false}, | ||||
|   {id: FILTER_CORRESPONDENT, name: "Correspondent is", filtervar: "correspondent__id", datatype: "correspondent", multi: false}, | ||||
|   {id: FILTER_DOCUMENT_TYPE, name: "Document type is", filtervar: "document_type__id", datatype: "document_type", multi: false}, | ||||
|  | ||||
|   {name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false},   | ||||
|   {name: "Has tag", filtervar: "tags__id__all", datatype: "tag", multi: true},   | ||||
|   {name: "Has any tag", filtervar: "is_tagged", datatype: "boolean", multi: false}, | ||||
|   {id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false},   | ||||
|   {id: FILTER_HAS_TAG, name: "Has tag", filtervar: "tags__id__all", datatype: "tag", multi: true},   | ||||
|   {id: FILTER_HAS_ANY_TAG, name: "Has any tag", filtervar: "is_tagged", datatype: "boolean", multi: false}, | ||||
|  | ||||
|   {name: "Created before", filtervar: "created__date__lt", datatype: "date", multi: false}, | ||||
|   {name: "Created after", filtervar: "created__date__gt", datatype: "date", multi: false}, | ||||
|   {id: FILTER_CREATED_BEFORE, name: "Created before", filtervar: "created__date__lt", datatype: "date", multi: false}, | ||||
|   {id: FILTER_CREATED_AFTER, name: "Created after", filtervar: "created__date__gt", datatype: "date", multi: false}, | ||||
|  | ||||
|   {name: "Year created is", filtervar: "created__year", datatype: "number", multi: false}, | ||||
|   {name: "Month created is", filtervar: "created__month", datatype: "number", multi: false}, | ||||
|   {name: "Day created is", filtervar: "created__day", datatype: "number", multi: false}, | ||||
|   {id: FILTER_CREATED_YEAR, name: "Year created is", filtervar: "created__year", datatype: "number", multi: false}, | ||||
|   {id: FILTER_CREATED_MONTH, name: "Month created is", filtervar: "created__month", datatype: "number", multi: false}, | ||||
|   {id: FILTER_CREATED_DAY, name: "Day created is", filtervar: "created__day", datatype: "number", multi: false}, | ||||
|  | ||||
|   {name: "Added before", filtervar: "added__date__lt", datatype: "date", multi: false}, | ||||
|   {name: "Added after", filtervar: "added__date__gt", datatype: "date", multi: false}, | ||||
|   {id: FILTER_ADDED_BEFORE, name: "Added before", filtervar: "added__date__lt", datatype: "date", multi: false}, | ||||
|   {id: FILTER_ADDED_AFTER, name: "Added after", filtervar: "added__date__gt", datatype: "date", multi: false}, | ||||
|    | ||||
|   {name: "Modified before", filtervar: "modified__date__lt", datatype: "date", multi: false}, | ||||
|   {name: "Modified after", filtervar: "modified__date__gt", datatype: "date", multi: false}, | ||||
|   {id: FILTER_MODIFIED_BEFORE, name: "Modified before", filtervar: "modified__date__lt", datatype: "date", multi: false}, | ||||
|   {id: FILTER_MODIFIED_AFTER, name: "Modified after", filtervar: "modified__date__gt", datatype: "date", multi: false}, | ||||
| ] | ||||
|  | ||||
| export interface FilterRuleType { | ||||
|   id: number | ||||
|   name: string | ||||
|   filtervar: string | ||||
|   datatype: string //number, string, boolean, date | ||||
|   | ||||
| @@ -7,6 +7,12 @@ import { DOCUMENT_LIST_SERVICE, GENERAL_SETTINGS } from '../data/storage-keys'; | ||||
| import { DocumentService } from './rest/document.service'; | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * This service manages the document list which is displayed using the document list view. | ||||
|  *  | ||||
|  * This service also serves saved views by transparently switching between the document list | ||||
|  * and saved views on request. See below. | ||||
|  */ | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
| }) | ||||
| @@ -14,80 +20,127 @@ export class DocumentListViewService { | ||||
|  | ||||
|   static DEFAULT_SORT_FIELD = 'created' | ||||
|  | ||||
|   isReloading: boolean = false | ||||
|   documents: PaperlessDocument[] = [] | ||||
|   currentPage = 1 | ||||
|   currentPageSize: number = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT | ||||
|   collectionSize: number | ||||
|    | ||||
|   private currentViewConfig: SavedViewConfig | ||||
|   //TODO: make private | ||||
|   viewConfigOverride: SavedViewConfig | ||||
|   /** | ||||
|    * This is the current config for the document list. The service will always remember the last settings used for the document list. | ||||
|    */ | ||||
|   private _documentListViewConfig: SavedViewConfig | ||||
|   /** | ||||
|    * Optionally, this is the currently selected saved view, which might be null. | ||||
|    */ | ||||
|   private _savedViewConfig: SavedViewConfig | ||||
|  | ||||
|   get viewId() { | ||||
|     return this.viewConfigOverride?.id | ||||
|   get savedView() { | ||||
|     return this._savedViewConfig | ||||
|   } | ||||
|  | ||||
|   set savedView(value) { | ||||
|     if (value) { | ||||
|       //this is here so that we don't modify value, which might be the actual instance of the saved view. | ||||
|       this._savedViewConfig = Object.assign({}, value) | ||||
|     } else { | ||||
|       this._savedViewConfig = null | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   get savedViewId() { | ||||
|     return this.savedView?.id | ||||
|   } | ||||
|  | ||||
|   get savedViewTitle() { | ||||
|     return this.savedView?.title | ||||
|   } | ||||
|  | ||||
|   get documentListView() { | ||||
|     return this._documentListViewConfig | ||||
|   } | ||||
|  | ||||
|   set documentListView(value) { | ||||
|     if (value) { | ||||
|       this._documentListViewConfig = Object.assign({}, value) | ||||
|       this.saveDocumentListView() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * This is what switches between the saved views and the document list view. Everything on the document list uses | ||||
|    * this property to determine the settings for the currently displayed document list. | ||||
|    */ | ||||
|   get view() { | ||||
|     return this.savedView || this.documentListView | ||||
|   } | ||||
|  | ||||
|   load(config: SavedViewConfig) { | ||||
|     this.view.filterRules = cloneFilterRules(config.filterRules) | ||||
|     this.view.sortDirection = config.sortDirection | ||||
|     this.view.sortField = config.sortField | ||||
|     this.reload() | ||||
|   } | ||||
|  | ||||
|   reload(onFinish?) { | ||||
|     let viewConfig = this.viewConfigOverride || this.currentViewConfig | ||||
|  | ||||
|     this.isReloading = true | ||||
|     this.documentService.list( | ||||
|       this.currentPage, | ||||
|       this.currentPageSize, | ||||
|       viewConfig.sortField, | ||||
|       viewConfig.sortDirection, | ||||
|       viewConfig.filterRules).subscribe( | ||||
|       this.view.sortField, | ||||
|       this.view.sortDirection, | ||||
|       this.view.filterRules).subscribe( | ||||
|         result => { | ||||
|           this.collectionSize = result.count | ||||
|           this.documents = result.results | ||||
|           if (onFinish) { | ||||
|             onFinish() | ||||
|           } | ||||
|           this.isReloading = false | ||||
|         }, | ||||
|         error => { | ||||
|           if (error.error['detail'] == 'Invalid page.') { | ||||
|             this.currentPage = 1 | ||||
|             this.reload() | ||||
|           } | ||||
|           this.isReloading = false | ||||
|         }) | ||||
|   } | ||||
|  | ||||
|   set filterRules(filterRules: FilterRule[]) { | ||||
|     this.currentViewConfig.filterRules = cloneFilterRules(filterRules) | ||||
|     this.saveCurrentViewConfig() | ||||
|     //we're going to clone the filterRules object, since we don't | ||||
|     //want changes in the filter editor to propagate into here right away. | ||||
|     this.view.filterRules = cloneFilterRules(filterRules) | ||||
|     this.reload() | ||||
|     this.saveDocumentListView() | ||||
|   } | ||||
|  | ||||
|   get filterRules(): FilterRule[] { | ||||
|     return cloneFilterRules(this.currentViewConfig.filterRules) | ||||
|     return cloneFilterRules(this.view.filterRules) | ||||
|   } | ||||
|  | ||||
|   set sortField(field: string) { | ||||
|     this.currentViewConfig.sortField = field | ||||
|     this.saveCurrentViewConfig() | ||||
|     this.view.sortField = field | ||||
|     this.saveDocumentListView() | ||||
|     this.reload() | ||||
|   } | ||||
|  | ||||
|   get sortField(): string { | ||||
|     return this.currentViewConfig.sortField | ||||
|     return this.view.sortField | ||||
|   } | ||||
|  | ||||
|   set sortDirection(direction: string) { | ||||
|     this.currentViewConfig.sortDirection = direction | ||||
|     this.saveCurrentViewConfig() | ||||
|     this.view.sortDirection = direction | ||||
|     this.saveDocumentListView() | ||||
|     this.reload() | ||||
|   } | ||||
|  | ||||
|   get sortDirection(): string { | ||||
|     return this.currentViewConfig.sortDirection | ||||
|     return this.view.sortDirection | ||||
|   } | ||||
|  | ||||
|   loadViewConfig(config: SavedViewConfig) { | ||||
|     Object.assign(this.currentViewConfig, config) | ||||
|     this.reload() | ||||
|   } | ||||
|  | ||||
|   private saveCurrentViewConfig() { | ||||
|     sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.currentViewConfig)) | ||||
|   private saveDocumentListView() { | ||||
|     sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.documentListView)) | ||||
|   } | ||||
|  | ||||
|   getLastPage(): number { | ||||
| @@ -134,21 +187,21 @@ export class DocumentListViewService { | ||||
|   } | ||||
|  | ||||
|   constructor(private documentService: DocumentService) {  | ||||
|     let currentViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG) | ||||
|     if (currentViewConfigJson) { | ||||
|     let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG) | ||||
|     if (documentListViewConfigJson) { | ||||
|       try { | ||||
|         this.currentViewConfig = JSON.parse(currentViewConfigJson) | ||||
|         this.documentListView = JSON.parse(documentListViewConfigJson) | ||||
|       } catch (e) { | ||||
|         sessionStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG) | ||||
|         this.currentViewConfig = null | ||||
|         this.documentListView = null | ||||
|       } | ||||
|     } | ||||
|     if (!this.currentViewConfig) { | ||||
|       this.currentViewConfig = { | ||||
|     if (!this.documentListView) { | ||||
|       this.documentListView = { | ||||
|         filterRules: [], | ||||
|         sortDirection: 'des', | ||||
|         sortField: 'created' | ||||
|       } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -36,13 +36,21 @@ export class SavedViewConfigService { | ||||
|     return this.configs.find(sf => sf.id == id) | ||||
|   } | ||||
|  | ||||
|   saveConfig(config: SavedViewConfig) { | ||||
|   newConfig(config: SavedViewConfig) { | ||||
|     config.id = uuidv4() | ||||
|     this.configs.push(config) | ||||
|  | ||||
|     this.save() | ||||
|   } | ||||
|  | ||||
|   updateConfig(config: SavedViewConfig) { | ||||
|     let savedConfig = this.configs.find(c => c.id == config.id) | ||||
|     if (savedConfig) { | ||||
|       Object.assign(savedConfig, config) | ||||
|       this.save() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private save() { | ||||
|     localStorage.setItem('saved-view-config-service:savedConfigs', JSON.stringify(this.configs)) | ||||
|   } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 jonaswinkler
					jonaswinkler