mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			fc4134e15c
			...
			feature-pr
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 4070cd0e1b | 
| @@ -1,10 +1,14 @@ | ||||
| <a [href]="link ?? previewUrl" class="{{linkClasses}}" [target]="linkTarget" [title]="linkTitle" | ||||
|   [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" container="body" | ||||
|   autoClose="true" [popoverClass]="popoverClass" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover"> | ||||
|   <ng-content></ng-content> | ||||
| </a> | ||||
| @if (!previewOnly) { | ||||
|   <a [href]="link ?? previewUrl" class="{{linkClasses}}" [target]="linkTarget" [title]="linkTitle" | ||||
|     [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" container="body" | ||||
|     autoClose="true" [popoverClass]="popoverClass" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover"> | ||||
|     <ng-content></ng-content> | ||||
|   </a> | ||||
| } @else { | ||||
|   <ng-container [ngTemplateOutlet]="previewContent" [ngTemplateOutletContext]="{ $implicit: document }"></ng-container> | ||||
| } | ||||
| <ng-template #previewContent> | ||||
|   <div class="preview-popup-container" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview(); close()"> | ||||
|   <div class="preview-popup-container" [class.full-size]="previewOnly" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview(); close()"> | ||||
|     @if (error) { | ||||
|       <div class="w-100 h-100 position-relative"> | ||||
|         <p class="fst-italic position-absolute top-50 start-50 translate-middle" i18n>Error loading preview</p> | ||||
|   | ||||
| @@ -4,6 +4,16 @@ | ||||
|     overflow-y: scroll; | ||||
| } | ||||
|  | ||||
| .preview-popup-container.full-size { | ||||
|   width: 100% !important; | ||||
|   height: 100% !important; | ||||
|  | ||||
|   > * { | ||||
|     width: 100% !important; | ||||
|     height: 100% !important; | ||||
|   } | ||||
| } | ||||
|  | ||||
| ::ng-deep .popover.popover-preview { | ||||
|     max-width: 32rem; | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import { NgTemplateOutlet } from '@angular/common' | ||||
| import { HttpClient } from '@angular/common/http' | ||||
| import { Component, Input, OnDestroy, ViewChild } from '@angular/core' | ||||
| import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' | ||||
| @@ -17,6 +18,7 @@ import { SettingsService } from 'src/app/services/settings.service' | ||||
|   styleUrls: ['./preview-popup.component.scss'], | ||||
|   imports: [ | ||||
|     NgbPopoverModule, | ||||
|     NgTemplateOutlet, | ||||
|     DocumentTitlePipe, | ||||
|     PdfViewerModule, | ||||
|     SafeUrlPipe, | ||||
| @@ -47,6 +49,9 @@ export class PreviewPopupComponent implements OnDestroy { | ||||
|   @Input() | ||||
|   linkTitle: string = $localize`Open preview` | ||||
|  | ||||
|   @Input() | ||||
|   previewOnly: boolean = false | ||||
|  | ||||
|   unsubscribeNotifier: Subject<any> = new Subject() | ||||
|  | ||||
|   error = false | ||||
| @@ -91,6 +96,8 @@ export class PreviewPopupComponent implements OnDestroy { | ||||
|   } | ||||
|  | ||||
|   init() { | ||||
|     this.error = false | ||||
|     this.requiresPassword = false | ||||
|     if (this.document.mime_type?.includes('text')) { | ||||
|       this.http | ||||
|         .get(this.previewURL, { responseType: 'text' }) | ||||
| @@ -119,6 +126,7 @@ export class PreviewPopupComponent implements OnDestroy { | ||||
|   } | ||||
|  | ||||
|   mouseEnterPreview() { | ||||
|     if (this.previewOnly) return | ||||
|     this.mouseOnPreview = true | ||||
|     if (!this.popover.isOpen()) { | ||||
|       // we're going to open but hide to pre-load content during hover delay | ||||
| @@ -136,10 +144,12 @@ export class PreviewPopupComponent implements OnDestroy { | ||||
|   } | ||||
|  | ||||
|   mouseLeavePreview() { | ||||
|     if (this.previewOnly) return | ||||
|     this.mouseOnPreview = false | ||||
|   } | ||||
|  | ||||
|   public close(immediate: boolean = false) { | ||||
|     if (this.previewOnly) return | ||||
|     setTimeout( | ||||
|       () => { | ||||
|         if (!this.mouseOnPreview) this.popover.close() | ||||
|   | ||||
| @@ -27,6 +27,7 @@ | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="btn-group flex-fill" role="group"> | ||||
|     <input type="radio" class="btn-check" [(ngModel)]="list.displayMode" value="table" id="displayModeDetails" name="displayModeDetails"> | ||||
|     <label for="displayModeDetails" class="btn btn-outline-primary btn-sm"> | ||||
| @@ -42,6 +43,13 @@ | ||||
|     </label> | ||||
|   </div> | ||||
|  | ||||
|   <div class="btn-group flex-fill" role="group"> | ||||
|     <input type="checkbox" class="btn-check" [(ngModel)]="list.showPreviewPane" value="table" id="previewPane" name="previewPane"> | ||||
|     <label for="previewPane" class="btn btn-outline-primary btn-sm"> | ||||
|       <i-bs name="window-split"></i-bs> | ||||
|     </label> | ||||
|   </div> | ||||
|  | ||||
|   <div ngbDropdown class="btn-group flex-fill"> | ||||
|     <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle> | ||||
|       <i-bs name="arrow-down-up"></i-bs> | ||||
| @@ -105,298 +113,335 @@ | ||||
|   <pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor> | ||||
| </div> | ||||
|  | ||||
|  | ||||
| <ng-template #pagination> | ||||
|   <div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3"> | ||||
|     <div class="d-flex align-items-center"> | ||||
|       @if (list.isReloading) { | ||||
|         <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|         <ng-container i18n>Loading...</ng-container> | ||||
|       } | ||||
|       @if (list.selected.size > 0) { | ||||
|         <span i18n>{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span> | ||||
|       } | ||||
|       @if (!list.isReloading) { | ||||
|         @if (list.selected.size === 0) { | ||||
|           <span i18n>{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span> | ||||
|           } @if (isFiltered) { | ||||
|              <span i18n>(filtered)</span> | ||||
| <div class="row"> | ||||
|   <div [class.col-lg-6]="list.showPreviewPane" [class.col]="!list.showPreviewPane"> | ||||
|   <ng-template #pagination> | ||||
|     <div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3"> | ||||
|       <div class="d-flex align-items-center"> | ||||
|         @if (list.isReloading) { | ||||
|           <div class="spinner-border spinner-border-sm me-2" role="status"></div> | ||||
|           <ng-container i18n>Loading...</ng-container> | ||||
|         } | ||||
|       } | ||||
|       @if (!list.isReloading && isFiltered) { | ||||
|         <button class="btn btn-link py-0" (click)="resetFilters()"> | ||||
|           <i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small> | ||||
|           </button> | ||||
|         @if (list.selected.size > 0) { | ||||
|           <span i18n>{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span> | ||||
|         } | ||||
|         @if (!list.isReloading) { | ||||
|           @if (list.selected.size === 0) { | ||||
|             <span i18n>{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span> | ||||
|             } @if (isFiltered) { | ||||
|                <span i18n>(filtered)</span> | ||||
|           } | ||||
|         } | ||||
|         @if (!list.isReloading && isFiltered) { | ||||
|           <button class="btn btn-link py-0" (click)="resetFilters()"> | ||||
|             <i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small> | ||||
|             </button> | ||||
|           } | ||||
|         </div> | ||||
|         @if (list.collectionSize) { | ||||
|           <ngb-pagination [pageSize]="list.pageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5" | ||||
|           [rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination> | ||||
|         } | ||||
|       </div> | ||||
|       @if (list.collectionSize) { | ||||
|         <ngb-pagination [pageSize]="list.pageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5" | ||||
|         [rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination> | ||||
|       } | ||||
|     </ng-template> | ||||
|  | ||||
|     <div tourAnchor="tour.documents"> | ||||
|       <ng-container *ngTemplateOutlet="pagination"></ng-container> | ||||
|     </div> | ||||
|   </ng-template> | ||||
|  | ||||
|   <div tourAnchor="tour.documents"> | ||||
|     <ng-container *ngTemplateOutlet="pagination"></ng-container> | ||||
|   </div> | ||||
|  | ||||
|   @if (list.error ) { | ||||
|     <div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div> | ||||
|   } @else { | ||||
|     @if (list.displayMode === DisplayMode.LARGE_CARDS) { | ||||
|       <div> | ||||
|         @for (d of list.documents; track d.id) { | ||||
|           <pngx-document-card-large | ||||
|             [selected]="list.isSelected(d)" | ||||
|             (toggleSelected)="toggleSelected(d, $event)" | ||||
|             (dblClickDocument)="openDocumentDetail(d)" | ||||
|             [document]="d" | ||||
|             [displayFields]="activeDisplayFields" | ||||
|             (clickTag)="clickTag($event)" | ||||
|             (clickCorrespondent)="clickCorrespondent($event)" | ||||
|             (clickDocumentType)="clickDocumentType($event)" | ||||
|             (clickStoragePath)="clickStoragePath($event)" | ||||
|             (clickMoreLike)="clickMoreLike(d.id)"> | ||||
|           </pngx-document-card-large> | ||||
|         } | ||||
|       </div> | ||||
|     } | ||||
|     @if (list.displayMode === DisplayMode.TABLE) { | ||||
|       <div class="table-responsive"> | ||||
|         <table class="table table-sm align-middle border shadow-sm"> | ||||
|           <thead> | ||||
|             <tr> | ||||
|               <th></th> | ||||
|               @if (activeDisplayFields.includes(DisplayField.ASN)) { | ||||
|                 <th class="cursor-pointer" | ||||
|                   pngxSortable="archive_serial_number" | ||||
|                   title="Sort by ASN" i18n-title | ||||
|                   [currentSortField]="list.sortField" | ||||
|                   [currentSortReverse]="list.sortReverse" | ||||
|                   (sort)="onSort($event)" | ||||
|                   i18n>ASN</th> | ||||
|               } | ||||
|               @if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) { | ||||
|                 <th class="cursor-pointer" | ||||
|                   pngxSortable="correspondent__name" | ||||
|                   title="Sort by correspondent" i18n-title | ||||
|                   [currentSortField]="list.sortField" | ||||
|                   [currentSortReverse]="list.sortReverse" | ||||
|                   (sort)="onSort($event)" | ||||
|                   i18n>Correspondent</th> | ||||
|               } | ||||
|               @if (activeDisplayFields.includes(DisplayField.TITLE)) { | ||||
|                 <th class="cursor-pointer" | ||||
|                   pngxSortable="title" | ||||
|                   title="Sort by title" i18n-title | ||||
|                   [currentSortField]="list.sortField" | ||||
|                   [currentSortReverse]="list.sortReverse" | ||||
|                   (sort)="onSort($event)" | ||||
|                   style="min-width: 150px;" | ||||
|                   i18n>Title</th> | ||||
|               } | ||||
|               @if (activeDisplayFields.includes(DisplayField.TAGS) && !activeDisplayFields.includes(DisplayField.TITLE)) { | ||||
|                 <th i18n>Tags</th> | ||||
|               } | ||||
|               @if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) { | ||||
|                 <th class="cursor-pointer" | ||||
|                   pngxSortable="owner" | ||||
|                   title="Sort by owner" i18n-title | ||||
|                   [currentSortField]="list.sortField" | ||||
|                   [currentSortReverse]="list.sortReverse" | ||||
|                   (sort)="onSort($event)" | ||||
|                   i18n>Owner</th> | ||||
|               } | ||||
|               @if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) { | ||||
|                 <th class="cursor-pointer" | ||||
|                   pngxSortable="num_notes" | ||||
|                   title="Sort by notes" i18n-title | ||||
|                   [currentSortField]="list.sortField" | ||||
|                   [currentSortReverse]="list.sortReverse" | ||||
|                   (sort)="onSort($event)" | ||||
|                   i18n>Notes</th> | ||||
|               } | ||||
|               @if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) { | ||||
|                 <th class="cursor-pointer" | ||||
|                   pngxSortable="document_type__name" | ||||
|                   title="Sort by document type" i18n-title | ||||
|                   [currentSortField]="list.sortField" | ||||
|                   [currentSortReverse]="list.sortReverse" | ||||
|                   (sort)="onSort($event)" | ||||
|                   i18n>Document type</th> | ||||
|               } | ||||
|               @if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) { | ||||
|                 <th class="cursor-pointer" | ||||
|                   pngxSortable="storage_path__name" | ||||
|                   title="Sort by storage path" i18n-title | ||||
|                   [currentSortField]="list.sortField" | ||||
|                   [currentSortReverse]="list.sortReverse" | ||||
|                   (sort)="onSort($event)" | ||||
|                   i18n>Storage path</th> | ||||
|               } | ||||
|               @if (activeDisplayFields.includes(DisplayField.CREATED)) { | ||||
|                 <th class="cursor-pointer" | ||||
|                   pngxSortable="created" | ||||
|                   title="Sort by created date" i18n-title | ||||
|                   [currentSortField]="list.sortField" | ||||
|                   [currentSortReverse]="list.sortReverse" | ||||
|                   (sort)="onSort($event)" | ||||
|                   i18n>Created</th> | ||||
|               } | ||||
|               @if (activeDisplayFields.includes(DisplayField.ADDED)) { | ||||
|                 <th class="cursor-pointer" | ||||
|                   pngxSortable="added" | ||||
|                   title="Sort by added date" i18n-title | ||||
|                   [currentSortField]="list.sortField" | ||||
|                   [currentSortReverse]="list.sortReverse" | ||||
|                   (sort)="onSort($event)" | ||||
|                   i18n>Added</th> | ||||
|               } | ||||
|               @if (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) { | ||||
|     @if (list.error ) { | ||||
|       <div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div> | ||||
|     } @else { | ||||
|       @if (list.displayMode === DisplayMode.LARGE_CARDS) { | ||||
|         <div> | ||||
|           @for (d of list.documents; track d.id) { | ||||
|             <pngx-document-card-large | ||||
|               [selected]="list.isSelected(d)" | ||||
|               (toggleSelected)="toggleSelected(d, $event)" | ||||
|               (dblClickDocument)="openDocumentDetail(d)" | ||||
|               [document]="d" | ||||
|               [displayFields]="activeDisplayFields" | ||||
|               (clickTag)="clickTag($event)" | ||||
|               (clickCorrespondent)="clickCorrespondent($event)" | ||||
|               (clickDocumentType)="clickDocumentType($event)" | ||||
|               (clickStoragePath)="clickStoragePath($event)" | ||||
|               (clickMoreLike)="clickMoreLike(d.id)"> | ||||
|             </pngx-document-card-large> | ||||
|           } | ||||
|         </div> | ||||
|       } | ||||
|       @if (list.displayMode === DisplayMode.TABLE) { | ||||
|         <div class="table-responsive"> | ||||
|           <table class="table table-sm align-middle border shadow-sm"> | ||||
|             <thead> | ||||
|               <tr> | ||||
|                 <th></th> | ||||
|                 @if (activeDisplayFields.includes(DisplayField.ASN)) { | ||||
|                   <th class="cursor-pointer" | ||||
|                     pngxSortable="page_count" | ||||
|                     title="Sort by number of pages" i18n-title | ||||
|                     pngxSortable="archive_serial_number" | ||||
|                     title="Sort by ASN" i18n-title | ||||
|                     [currentSortField]="list.sortField" | ||||
|                     [currentSortReverse]="list.sortReverse" | ||||
|                     (sort)="onSort($event)" | ||||
|                     i18n>Pages</th> | ||||
|                 } | ||||
|               @if (activeDisplayFields.includes(DisplayField.SHARED)) { | ||||
|                 <th i18n> | ||||
|                   Shared | ||||
|                 </th> | ||||
|               } | ||||
|               @for (field_id of activeDisplayCustomFields; track field_id) { | ||||
|                 <th class="cursor-pointer" | ||||
|                   pngxSortable="{{field_id}}" | ||||
|                   title="Sort by {{getDisplayCustomFieldTitle(field_id)}}" i18n-title | ||||
|                   [currentSortField]="list.sortField" | ||||
|                   [currentSortReverse]="list.sortReverse" | ||||
|                   (sort)="onSort($event)"> | ||||
|                   {{getDisplayCustomFieldTitle(field_id)}} | ||||
|                 </th> | ||||
|               } | ||||
|             </tr> | ||||
|           </thead> | ||||
|           <tbody> | ||||
|             @for (d of list.documents; track d.id) { | ||||
|               <tr (click)="toggleSelected(d, $event); $event.stopPropagation();" (dblclick)="openDocumentDetail(d)" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''"> | ||||
|                 <td> | ||||
|                   <div class="form-check"> | ||||
|                     <input type="checkbox" class="form-check-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event); $event.stopPropagation();"> | ||||
|                     <label class="form-check-label" for="docCheck{{d.id}}"></label> | ||||
|                   </div> | ||||
|                 </td> | ||||
|                 @if (activeDisplayFields.includes(DisplayField.ASN)) { | ||||
|                   <td class=""> | ||||
|                     {{d.archive_serial_number}} | ||||
|                   </td> | ||||
|                     i18n>ASN</th> | ||||
|                 } | ||||
|                 @if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) { | ||||
|                   <td class=""> | ||||
|                     @if (d.correspondent) { | ||||
|                       <a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a> | ||||
|                     } | ||||
|                   </td> | ||||
|                   <th class="cursor-pointer" | ||||
|                     pngxSortable="correspondent__name" | ||||
|                     title="Sort by correspondent" i18n-title | ||||
|                     [currentSortField]="list.sortField" | ||||
|                     [currentSortReverse]="list.sortReverse" | ||||
|                     (sort)="onSort($event)" | ||||
|                     i18n>Correspondent</th> | ||||
|                 } | ||||
|                 @if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) { | ||||
|                   <td width="30%"> | ||||
|                     @if (activeDisplayFields.includes(DisplayField.TITLE)) { | ||||
|                       <div class="d-inline-block" (mouseleave)="popupPreview.close()"> | ||||
|                         <a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a> | ||||
|                         <pngx-preview-popup [document]="d" linkClasses="btn btn-sm btn-link text-secondary" linkTitle="Preview document" (click)="$event.stopPropagation()" i18n-linkTitle #popupPreview> | ||||
|                           <i-bs name="eye"></i-bs> | ||||
|                         </pngx-preview-popup> | ||||
|                       </div> | ||||
|                     } | ||||
|                     @if (activeDisplayFields.includes(DisplayField.TAGS)) { | ||||
|                       @for (t of d.tags$ | async; track t) { | ||||
|                         <pngx-tag [tag]="t" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag> | ||||
|                       } | ||||
|                     } | ||||
|                   </td> | ||||
|                 @if (activeDisplayFields.includes(DisplayField.TITLE)) { | ||||
|                   <th class="cursor-pointer" | ||||
|                     pngxSortable="title" | ||||
|                     title="Sort by title" i18n-title | ||||
|                     [currentSortField]="list.sortField" | ||||
|                     [currentSortReverse]="list.sortReverse" | ||||
|                     (sort)="onSort($event)" | ||||
|                     style="min-width: 150px;" | ||||
|                     i18n>Title</th> | ||||
|                 } | ||||
|                 @if (activeDisplayFields.includes(DisplayField.TAGS) && !activeDisplayFields.includes(DisplayField.TITLE)) { | ||||
|                   <th i18n>Tags</th> | ||||
|                 } | ||||
|                 @if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) { | ||||
|                   <td> | ||||
|                     {{d.owner | username}} | ||||
|                   </td> | ||||
|                   <th class="cursor-pointer" | ||||
|                     pngxSortable="owner" | ||||
|                     title="Sort by owner" i18n-title | ||||
|                     [currentSortField]="list.sortField" | ||||
|                     [currentSortReverse]="list.sortReverse" | ||||
|                     (sort)="onSort($event)" | ||||
|                     i18n>Owner</th> | ||||
|                 } | ||||
|                 @if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) { | ||||
|                   <td class=""> | ||||
|                     @if (d.notes.length) { | ||||
|                       <a routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0"> | ||||
|                         <span class="badge rounded-pill bg-light border text-primary"> | ||||
|                           <i-bs width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></i-bs> | ||||
|                         {{d.notes.length}}</span> | ||||
|                       </a> | ||||
|                     } | ||||
|                   </td> | ||||
|                   <th class="cursor-pointer" | ||||
|                     pngxSortable="num_notes" | ||||
|                     title="Sort by notes" i18n-title | ||||
|                     [currentSortField]="list.sortField" | ||||
|                     [currentSortReverse]="list.sortReverse" | ||||
|                     (sort)="onSort($event)" | ||||
|                     i18n>Notes</th> | ||||
|                 } | ||||
|                 @if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) { | ||||
|                   <td class=""> | ||||
|                     @if (d.document_type) { | ||||
|                       <a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a> | ||||
|                     } | ||||
|                   </td> | ||||
|                   <th class="cursor-pointer" | ||||
|                     pngxSortable="document_type__name" | ||||
|                     title="Sort by document type" i18n-title | ||||
|                     [currentSortField]="list.sortField" | ||||
|                     [currentSortReverse]="list.sortReverse" | ||||
|                     (sort)="onSort($event)" | ||||
|                     i18n>Document type</th> | ||||
|                 } | ||||
|                 @if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) { | ||||
|                   <td class=""> | ||||
|                     @if (d.storage_path) { | ||||
|                       <a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a> | ||||
|                     } | ||||
|                   </td> | ||||
|                   <th class="cursor-pointer" | ||||
|                     pngxSortable="storage_path__name" | ||||
|                     title="Sort by storage path" i18n-title | ||||
|                     [currentSortField]="list.sortField" | ||||
|                     [currentSortReverse]="list.sortReverse" | ||||
|                     (sort)="onSort($event)" | ||||
|                     i18n>Storage path</th> | ||||
|                 } | ||||
|                 @if (activeDisplayFields.includes(DisplayField.CREATED)) { | ||||
|                   <td> | ||||
|                     {{d.created_date | customDate}} | ||||
|                   </td> | ||||
|                   <th class="cursor-pointer" | ||||
|                     pngxSortable="created" | ||||
|                     title="Sort by created date" i18n-title | ||||
|                     [currentSortField]="list.sortField" | ||||
|                     [currentSortReverse]="list.sortReverse" | ||||
|                     (sort)="onSort($event)" | ||||
|                     i18n>Created</th> | ||||
|                 } | ||||
|                 @if (activeDisplayFields.includes(DisplayField.ADDED)) { | ||||
|                   <td> | ||||
|                     {{d.added | customDate}} | ||||
|                   </td> | ||||
|                   <th class="cursor-pointer" | ||||
|                     pngxSortable="added" | ||||
|                     title="Sort by added date" i18n-title | ||||
|                     [currentSortField]="list.sortField" | ||||
|                     [currentSortReverse]="list.sortReverse" | ||||
|                     (sort)="onSort($event)" | ||||
|                     i18n>Added</th> | ||||
|                 } | ||||
|                 @if (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) { | ||||
|                     <td> | ||||
|                         {{ d.page_count }} | ||||
|                     </td> | ||||
|                     <th class="cursor-pointer" | ||||
|                       pngxSortable="page_count" | ||||
|                       title="Sort by number of pages" i18n-title | ||||
|                       [currentSortField]="list.sortField" | ||||
|                       [currentSortReverse]="list.sortReverse" | ||||
|                       (sort)="onSort($event)" | ||||
|                       i18n>Pages</th> | ||||
|                   } | ||||
|                 @if (activeDisplayFields.includes(DisplayField.SHARED)) { | ||||
|                   <td> | ||||
|                     @if (d.is_shared_by_requester) { <ng-container i18n>Yes</ng-container> } @else { <ng-container i18n>No</ng-container> } | ||||
|                   </td> | ||||
|                   <th i18n> | ||||
|                     Shared | ||||
|                   </th> | ||||
|                 } | ||||
|                 @for (field of activeDisplayCustomFields; track field) { | ||||
|                   <td class=""> | ||||
|                     <pngx-custom-field-display [document]="d" [fieldDisplayKey]="field"></pngx-custom-field-display> | ||||
|                   </td> | ||||
|                 @for (field_id of activeDisplayCustomFields; track field_id) { | ||||
|                   <th class="cursor-pointer" | ||||
|                     pngxSortable="{{field_id}}" | ||||
|                     title="Sort by {{getDisplayCustomFieldTitle(field_id)}}" i18n-title | ||||
|                     [currentSortField]="list.sortField" | ||||
|                     [currentSortReverse]="list.sortReverse" | ||||
|                     (sort)="onSort($event)"> | ||||
|                     {{getDisplayCustomFieldTitle(field_id)}} | ||||
|                   </th> | ||||
|                 } | ||||
|               </tr> | ||||
|             } | ||||
|           </tbody> | ||||
|         </table> | ||||
|       </div> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|               @for (d of list.documents; track d.id) { | ||||
|                 <tr (click)="toggleSelected(d, $event); $event.stopPropagation();" (dblclick)="openDocumentDetail(d)" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''"> | ||||
|                   <td> | ||||
|                     <div class="form-check"> | ||||
|                       <input type="checkbox" class="form-check-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event); $event.stopPropagation();"> | ||||
|                       <label class="form-check-label" for="docCheck{{d.id}}"></label> | ||||
|                     </div> | ||||
|                   </td> | ||||
|                   @if (activeDisplayFields.includes(DisplayField.ASN)) { | ||||
|                     <td class=""> | ||||
|                       {{d.archive_serial_number}} | ||||
|                     </td> | ||||
|                   } | ||||
|                   @if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) { | ||||
|                     <td class=""> | ||||
|                       @if (d.correspondent) { | ||||
|                         <a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a> | ||||
|                       } | ||||
|                     </td> | ||||
|                   } | ||||
|                   @if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) { | ||||
|                     <td width="30%"> | ||||
|                       @if (activeDisplayFields.includes(DisplayField.TITLE)) { | ||||
|                         <div class="d-inline-block" (mouseleave)="popupPreview.close()"> | ||||
|                           <a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a> | ||||
|                           <pngx-preview-popup [document]="d" linkClasses="btn btn-sm btn-link text-secondary" linkTitle="Preview document" (click)="$event.stopPropagation()" i18n-linkTitle #popupPreview> | ||||
|                             <i-bs name="eye"></i-bs> | ||||
|                           </pngx-preview-popup> | ||||
|                         </div> | ||||
|                       } | ||||
|                       @if (activeDisplayFields.includes(DisplayField.TAGS)) { | ||||
|                         @for (t of d.tags$ | async; track t) { | ||||
|                           <pngx-tag [tag]="t" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag> | ||||
|                         } | ||||
|                       } | ||||
|                     </td> | ||||
|                   } | ||||
|                   @if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) { | ||||
|                     <td> | ||||
|                       {{d.owner | username}} | ||||
|                     </td> | ||||
|                   } | ||||
|                   @if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) { | ||||
|                     <td class=""> | ||||
|                       @if (d.notes.length) { | ||||
|                         <a routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0"> | ||||
|                           <span class="badge rounded-pill bg-light border text-primary"> | ||||
|                             <i-bs width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></i-bs> | ||||
|                           {{d.notes.length}}</span> | ||||
|                         </a> | ||||
|                       } | ||||
|                     </td> | ||||
|                   } | ||||
|                   @if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) { | ||||
|                     <td class=""> | ||||
|                       @if (d.document_type) { | ||||
|                         <a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a> | ||||
|                       } | ||||
|                     </td> | ||||
|                   } | ||||
|                   @if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) { | ||||
|                     <td class=""> | ||||
|                       @if (d.storage_path) { | ||||
|                         <a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a> | ||||
|                       } | ||||
|                     </td> | ||||
|                   } | ||||
|                   @if (activeDisplayFields.includes(DisplayField.CREATED)) { | ||||
|                     <td> | ||||
|                       {{d.created_date | customDate}} | ||||
|                     </td> | ||||
|                   } | ||||
|                   @if (activeDisplayFields.includes(DisplayField.ADDED)) { | ||||
|                     <td> | ||||
|                       {{d.added | customDate}} | ||||
|                     </td> | ||||
|                   } | ||||
|                   @if (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) { | ||||
|                       <td> | ||||
|                           {{ d.page_count }} | ||||
|                       </td> | ||||
|                     } | ||||
|                   @if (activeDisplayFields.includes(DisplayField.SHARED)) { | ||||
|                     <td> | ||||
|                       @if (d.is_shared_by_requester) { <ng-container i18n>Yes</ng-container> } @else { <ng-container i18n>No</ng-container> } | ||||
|                     </td> | ||||
|                   } | ||||
|                   @for (field of activeDisplayCustomFields; track field) { | ||||
|                     <td class=""> | ||||
|                       <pngx-custom-field-display [document]="d" [fieldDisplayKey]="field"></pngx-custom-field-display> | ||||
|                     </td> | ||||
|                   } | ||||
|                 </tr> | ||||
|               } | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </div> | ||||
|       } | ||||
|       @if (list.displayMode === DisplayMode.SMALL_CARDS) { | ||||
|         <div class="row row-cols-paperless-cards"> | ||||
|           @for (d of list.documents; track d.id) { | ||||
|             <pngx-document-card-small class="p-0" | ||||
|               [selected]="list.isSelected(d)" | ||||
|               (toggleSelected)="toggleSelected(d, $event)" | ||||
|               (dblClickDocument)="openDocumentDetail(d)" | ||||
|               [document]="d" | ||||
|               (clickTag)="clickTag($event)" | ||||
|               [displayFields]="activeDisplayFields" | ||||
|               (clickCorrespondent)="clickCorrespondent($event)" | ||||
|               (clickStoragePath)="clickStoragePath($event)" | ||||
|               (clickDocumentType)="clickDocumentType($event)"> | ||||
|             </pngx-document-card-small> | ||||
|           } | ||||
|         </div> | ||||
|       } | ||||
|       @if (list.documents?.length > 15) { | ||||
|         <div class="mt-3"> | ||||
|           <ng-container *ngTemplateOutlet="pagination"></ng-container> | ||||
|         </div> | ||||
|       } | ||||
|     } | ||||
|     @if (list.displayMode === DisplayMode.SMALL_CARDS) { | ||||
|       <div class="row row-cols-paperless-cards"> | ||||
|         @for (d of list.documents; track d.id) { | ||||
|           <pngx-document-card-small class="p-0" | ||||
|             [selected]="list.isSelected(d)" | ||||
|             (toggleSelected)="toggleSelected(d, $event)" | ||||
|             (dblClickDocument)="openDocumentDetail(d)" | ||||
|             [document]="d" | ||||
|             (clickTag)="clickTag($event)" | ||||
|             [displayFields]="activeDisplayFields" | ||||
|             (clickCorrespondent)="clickCorrespondent($event)" | ||||
|             (clickStoragePath)="clickStoragePath($event)" | ||||
|             (clickDocumentType)="clickDocumentType($event)"> | ||||
|           </pngx-document-card-small> | ||||
|         } | ||||
|   </div> | ||||
|   @if (list.showPreviewPane) { | ||||
|     <div class="col-lg-6"> | ||||
|       <div class="row"> | ||||
|         <div class="btn-toolbar mb-1 border-bottom align-items-center"> | ||||
|           <div class="btn-group pb-3"> | ||||
|             <button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Previous" (click)="previousDoc()" [disabled]="list.documents.length === 0 || !hasPrevious"> | ||||
|               <i-bs width="1.2em" height="1.2em" name="arrow-left" class="me-1"></i-bs><ng-container i18n>Previous</ng-container> | ||||
|             </button> | ||||
|             <button type="button" class="btn btn-sm btn-outline-secondary"  i18n-title title="Next" (click)="nextDoc()" [disabled]="list.documents.length === 0 || !hasNext"> | ||||
|               <ng-container i18n>Next</ng-container><i-bs width="1.2em" height="1.2em" name="arrow-right" class="ms-1"></i-bs> | ||||
|             </button> | ||||
|           </div> | ||||
|           <div class="input-group pb-3 ms-auto"> | ||||
|             <h5 class="mb-0"> | ||||
|               {{list.firstSelectedDocument?.title}} | ||||
|             </h5> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     } | ||||
|     @if (list.documents?.length > 15) { | ||||
|       <div class="mt-3"> | ||||
|         <ng-container *ngTemplateOutlet="pagination"></ng-container> | ||||
|       <div class="row"> | ||||
|         <div class="col preview-pane"> | ||||
|           @if (list.selected.size > 0) { | ||||
|             <pngx-preview-popup [document]="list.firstSelectedDocument" [previewOnly]="true"></pngx-preview-popup> | ||||
|           } @else { | ||||
|             <div class="w-100 h-100 position-relative"> | ||||
|               <p class="fst-italic"> | ||||
|                 <ng-container i18n>No document selected</ng-container> | ||||
|               </p> | ||||
|             </div> | ||||
|           } | ||||
|         </div> | ||||
|       </div> | ||||
|     } | ||||
|     </div> | ||||
|   } | ||||
| </div> | ||||
|   | ||||
| @@ -80,3 +80,9 @@ a { | ||||
| pngx-page-header .dropdown-menu { | ||||
|   --bs-dropdown-min-width: 12em; | ||||
| } | ||||
|  | ||||
| .preview-pane { | ||||
|   height: 60rem; | ||||
|   top: 70px; | ||||
|   position: sticky; | ||||
| } | ||||
|   | ||||
| @@ -326,24 +326,36 @@ export class DocumentListComponent | ||||
|     this.hotKeyService | ||||
|       .addShortcut({ | ||||
|         keys: 'control.arrowleft', | ||||
|         description: $localize`Previous page`, | ||||
|         description: $localize`Previous page / document`, | ||||
|       }) | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe(() => { | ||||
|         if (this.list.currentPage > 1) { | ||||
|           this.list.currentPage-- | ||||
|         if (this.list.showPreviewPane) { | ||||
|           if (this.hasPrevious) { | ||||
|             this.previousDoc() | ||||
|           } | ||||
|         } else { | ||||
|           if (this.list.currentPage > 1) { | ||||
|             this.list.currentPage-- | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|  | ||||
|     this.hotKeyService | ||||
|       .addShortcut({ | ||||
|         keys: 'control.arrowright', | ||||
|         description: $localize`Next page`, | ||||
|         description: $localize`Next page / document`, | ||||
|       }) | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe(() => { | ||||
|         if (this.list.currentPage < this.list.getLastPage()) { | ||||
|           this.list.currentPage++ | ||||
|         if (this.list.showPreviewPane) { | ||||
|           if (this.hasNext) { | ||||
|             this.nextDoc() | ||||
|           } | ||||
|         } else { | ||||
|           if (this.list.currentPage < this.list.getLastPage()) { | ||||
|             this.list.currentPage++ | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|   } | ||||
| @@ -473,4 +485,45 @@ export class DocumentListComponent | ||||
|   resetFilters() { | ||||
|     this.filterEditor.resetSelected() | ||||
|   } | ||||
|  | ||||
|   public get hasPrevious(): boolean { | ||||
|     return ( | ||||
|       (this.list.selected.size > 0 && | ||||
|         this.list.documents.indexOf(this.list.firstSelectedDocument) > 0) || | ||||
|       (this.list.selected.size === 0 && this.list.documents.length > 0) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   public get hasNext(): boolean { | ||||
|     return ( | ||||
|       (this.list.selected.size > 0 && | ||||
|         this.list.documents.indexOf(this.list.firstSelectedDocument) < | ||||
|           this.list.documents.length - 1) || | ||||
|       (this.list.selected.size === 0 && this.list.documents.length > 0) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   public nextDoc(): void { | ||||
|     const index = | ||||
|       this.list.selected.size === 0 | ||||
|         ? 0 | ||||
|         : Math.min( | ||||
|             this.list.documents.indexOf(this.list.firstSelectedDocument) + 1, | ||||
|             this.list.documents.length - 1 | ||||
|           ) | ||||
|     this.list.selected.clear() | ||||
|     this.list.selected.add(this.list.documents[index].id) | ||||
|   } | ||||
|  | ||||
|   public previousDoc(): void { | ||||
|     const index = | ||||
|       this.list.selected.size === 0 | ||||
|         ? 0 | ||||
|         : Math.max( | ||||
|             this.list.documents.indexOf(this.list.firstSelectedDocument) - 1, | ||||
|             0 | ||||
|           ) | ||||
|     this.list.selected.clear() | ||||
|     this.list.selected.add(this.list.documents[index].id) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -79,6 +79,11 @@ export interface ListViewState { | ||||
|    * The fields to display in the document list. | ||||
|    */ | ||||
|   displayFields?: DisplayField[] | ||||
|  | ||||
|   /** | ||||
|    * Whether the preview pane is shown. | ||||
|    */ | ||||
|   showPreviewPane?: boolean | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -165,6 +170,7 @@ export class DocumentListViewService { | ||||
|       sortReverse: true, | ||||
|       filterRules: [], | ||||
|       selected: new Set<number>(), | ||||
|       showPreviewPane: false, | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -451,6 +457,15 @@ export class DocumentListViewService { | ||||
|     this.saveDocumentListView() | ||||
|   } | ||||
|  | ||||
|   get showPreviewPane(): boolean { | ||||
|     return this.activeListViewState.showPreviewPane | ||||
|   } | ||||
|  | ||||
|   set showPreviewPane(show: boolean) { | ||||
|     this.activeListViewState.showPreviewPane = show | ||||
|     this.saveDocumentListView() | ||||
|   } | ||||
|  | ||||
|   private saveDocumentListView() { | ||||
|     if (this._activeSavedViewId == null) { | ||||
|       let savedState: ListViewState = { | ||||
| @@ -461,6 +476,7 @@ export class DocumentListViewService { | ||||
|         sortReverse: this.activeListViewState.sortReverse, | ||||
|         displayMode: this.activeListViewState.displayMode, | ||||
|         displayFields: this.activeListViewState.displayFields, | ||||
|         showPreviewPane: this.activeListViewState.showPreviewPane, | ||||
|       } | ||||
|       localStorage.setItem( | ||||
|         DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, | ||||
| @@ -626,4 +642,8 @@ export class DocumentListViewService { | ||||
|   documentIndexInCurrentView(documentID: number): number { | ||||
|     return this.documents.map((d) => d.id).indexOf(documentID) | ||||
|   } | ||||
|  | ||||
|   get firstSelectedDocument(): Document { | ||||
|     return this.documents.find((d) => this.selected.has(d.id)) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -125,6 +125,7 @@ import { | ||||
|   trash, | ||||
|   uiRadios, | ||||
|   upcScan, | ||||
|   windowSplit, | ||||
|   windowStack, | ||||
|   x, | ||||
|   xCircle, | ||||
| @@ -323,6 +324,7 @@ const icons = { | ||||
|   trash, | ||||
|   uiRadios, | ||||
|   upcScan, | ||||
|   windowSplit, | ||||
|   windowStack, | ||||
|   x, | ||||
|   xCircle, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user