]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Just save this feature-preview-pane-7117
authorshamoon <4887959+shamoon@users.noreply.github.com>
Wed, 22 Jan 2025 20:13:35 +0000 (12:13 -0800)
committershamoon <4887959+shamoon@users.noreply.github.com>
Wed, 22 Jan 2025 20:13:35 +0000 (12:13 -0800)
[ci skip]

src-ui/src/app/components/common/preview-popup/preview-popup.component.html
src-ui/src/app/components/common/preview-popup/preview-popup.component.scss
src-ui/src/app/components/common/preview-popup/preview-popup.component.ts
src-ui/src/app/components/document-list/document-list.component.html
src-ui/src/app/components/document-list/document-list.component.scss
src-ui/src/app/components/document-list/document-list.component.ts
src-ui/src/app/services/document-list-view.service.ts
src-ui/src/main.ts

index 59c179832dec3a23b00b94a4ce3eabea183aabb1..8261db465e0b3fd8bf6b8c51191bb618d8956e5a 100644 (file)
@@ -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>
index af8dc565a239d563a087a87eb552e402f5aee338..ab8f8067c206e3e07cda24065415c80e135c82d9 100644 (file)
@@ -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;
 }
index 039264fd0d900b5fb2d2999547ffacab3fb139e9..e2c0edb6c8d11a094a69ce49c9ce7bc725580bf0 100644 (file)
@@ -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()
index 65d291464b2e3c278bd15382289c7051d1e3cc7a..abe00a733626592e7207aca6bf3f2e4aeadd6539 100644 (file)
@@ -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">
     </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>
   <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>
-          }&nbsp;@if (isFiltered) {
-            &nbsp;<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>
+            }&nbsp;@if (isFiltered) {
+              &nbsp;<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>
-      }
-    </div>
-  </ng-template>
+    </ng-template>
 
-  <div tourAnchor="tour.documents">
-    <ng-container *ngTemplateOutlet="pagination"></ng-container>
-  </div>
+    <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>
index 0e10b83daf21ff40894a4a550a7f071b57c6709a..29512efaf45977bc1b2d8a363f3fc806011011e5 100644 (file)
@@ -80,3 +80,9 @@ a {
 pngx-page-header .dropdown-menu {
   --bs-dropdown-min-width: 12em;
 }
+
+.preview-pane {
+  height: 60rem;
+  top: 70px;
+  position: sticky;
+}
index a19ac341df84ac9472dcc4b7f27cc27d04ebf4cf..b2617ecbf5c02d8607748667c3b74e77f8a53c33 100644 (file)
@@ -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)
+  }
 }
index 09d83a2fbc017aa1219052c173a8909eb3b29e5a..bea73578d4a6e355732055f113bd2aa0d0947ab0 100644 (file)
@@ -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))
+  }
 }
index 998ebf260902e2a43372edf17e449d7bb2bf1fc2..774bdcb108644dd51a3f9dd4b783e8d410a5264a 100644 (file)
@@ -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,