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