]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement: preview button for document list and trash, refactor (#8384)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Sat, 30 Nov 2024 05:24:33 +0000 (21:24 -0800)
committerGitHub <noreply@github.com>
Sat, 30 Nov 2024 05:24:33 +0000 (21:24 -0800)
14 files changed:
src-ui/messages.xlf
src-ui/src/app/components/admin/trash/trash.component.html
src-ui/src/app/components/common/preview-popup/preview-popup.component.html
src-ui/src/app/components/common/preview-popup/preview-popup.component.spec.ts
src-ui/src/app/components/common/preview-popup/preview-popup.component.ts
src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html
src-ui/src/app/components/document-list/document-card-large/document-card-large.component.spec.ts
src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts
src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html
src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts
src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts
src-ui/src/app/components/document-list/document-list.component.html
src-ui/src/styles.scss
src/documents/views.py

index 0126476059ddda2ee21c1d6dff3665306a4825d6..bd8b890950e864fa750e3ab5a91bf25cefc682e9 100644 (file)
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
-          <context context-type="linenumber">67</context>
+          <context context-type="linenumber">72</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
-          <context context-type="linenumber">76</context>
+          <context context-type="linenumber">81</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
         <source><x id="INTERPOLATION" equiv-text="{{ getDaysRemaining(document) }}"/> days</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
-          <context context-type="linenumber">58</context>
+          <context context-type="linenumber">63</context>
         </context-group>
       </trans-unit>
       <trans-unit id="6770769801335635194" datatype="html">
         <source>Restore</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
-          <context context-type="linenumber">66</context>
+          <context context-type="linenumber">71</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
-          <context context-type="linenumber">73</context>
+          <context context-type="linenumber">78</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2308646316372333720" datatype="html">
         <source>{VAR_PLURAL, plural, =1 {One document in trash} other {<x id="INTERPOLATION"/> total documents in trash}}</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
-          <context context-type="linenumber">89</context>
+          <context context-type="linenumber">94</context>
         </context-group>
       </trans-unit>
       <trans-unit id="9021887951960049161" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
-          <context context-type="linenumber">68</context>
+          <context context-type="linenumber">63</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
-          <context context-type="linenumber">140</context>
+          <context context-type="linenumber">135</context>
         </context-group>
       </trans-unit>
       <trans-unit id="searchResults.noResults" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
-          <context context-type="linenumber">62</context>
+          <context context-type="linenumber">60</context>
         </context-group>
       </trans-unit>
       <trans-unit id="9195188695728229921" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.ts</context>
-          <context context-type="linenumber">86</context>
+          <context context-type="linenumber">79</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2504502765849142619" datatype="html">
         <source>Error loading preview</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/common/preview-popup/preview-popup.component.html</context>
-          <context context-type="linenumber">4</context>
+          <context context-type="linenumber">10</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="3601402187462260332" datatype="html">
+        <source>Open preview</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/common/preview-popup/preview-popup.component.ts</context>
+          <context context-type="linenumber">37</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2984628903434675339" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
-          <context context-type="linenumber">79</context>
+          <context context-type="linenumber">74</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
-          <context context-type="linenumber">323</context>
+          <context context-type="linenumber">328</context>
         </context-group>
       </trans-unit>
       <trans-unit id="157572966557284263" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
-          <context context-type="linenumber">85</context>
+          <context context-type="linenumber">80</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
-          <context context-type="linenumber">330</context>
+          <context context-type="linenumber">335</context>
         </context-group>
       </trans-unit>
       <trans-unit id="8911158217491828773" datatype="html">
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
-          <context context-type="linenumber">299</context>
+          <context context-type="linenumber">304</context>
         </context-group>
       </trans-unit>
       <trans-unit id="106713086593101376" datatype="html">
         <source>View notes</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
-          <context context-type="linenumber">74</context>
+          <context context-type="linenumber">69</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3727324658595204357" datatype="html">
         <source>Created: <x id="INTERPOLATION" equiv-text="{{ document.created_date | customDate }}"/></source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
-          <context context-type="linenumber">98,99</context>
+          <context context-type="linenumber">93,94</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
         <source>Added: <x id="INTERPOLATION" equiv-text="{{ document.added | customDate }}"/></source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
-          <context context-type="linenumber">99,100</context>
+          <context context-type="linenumber">94,95</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
         <source>Modified: <x id="INTERPOLATION" equiv-text="{{ document.modified | customDate }}"/></source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
-          <context context-type="linenumber">100,101</context>
+          <context context-type="linenumber">95,96</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
         <source>{VAR_PLURAL, plural, =1 {1 page} other {<x id="INTERPOLATION"/> pages}}</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
-          <context context-type="linenumber">117</context>
+          <context context-type="linenumber">112</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
         <source>Shared</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
-          <context context-type="linenumber">127</context>
+          <context context-type="linenumber">122</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
         <source>Score:</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
-          <context context-type="linenumber">132</context>
+          <context context-type="linenumber">127</context>
         </context-group>
       </trans-unit>
       <trans-unit id="3661756380991326939" datatype="html">
         <source>Edit document</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
-          <context context-type="linenumber">295</context>
+          <context context-type="linenumber">296</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="3420321797707163677" datatype="html">
+        <source>Preview document</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
+          <context context-type="linenumber">297</context>
         </context-group>
       </trans-unit>
       <trans-unit id="2807800733729323332" datatype="html">
         <source>Yes</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
-          <context context-type="linenumber">351</context>
+          <context context-type="linenumber">356</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/pipes/yes-no.pipe.ts</context>
         <source>No</source>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
-          <context context-type="linenumber">351</context>
+          <context context-type="linenumber">356</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">src/app/pipes/yes-no.pipe.ts</context>
index 1c66bdd441724ff1e41c9a015b0b2dcbadee023a..3dbf3db2be5a98e2ddda456067077664e7ba47b0 100644 (file)
                 </tr>
             }
             @for (document of documentsInTrash; track document.id) {
-                <tr (click)="toggleSelected(document); $event.stopPropagation();">
+                <tr (click)="toggleSelected(document); $event.stopPropagation();" (mouseleave)="popupPreview.close()">
                     <td>
                     <div class="form-check m-0 ms-2 me-n2">
                         <input type="checkbox" class="form-check-input" id="{{document.id}}" [checked]="selectedDocuments.has(document.id)" (click)="toggleSelected(document); $event.stopPropagation();">
                         <label class="form-check-label" for="{{document.id}}"></label>
                     </div>
                     </td>
-                    <td scope="row">{{ document.title }}</td>
+                    <td scope="row">
+                      {{ document.title }}
+                      <pngx-preview-popup [document]="document" linkClasses="btn btn-sm btn-link" #popupPreview>
+                        <i-bs name="eye"></i-bs>
+                      </pngx-preview-popup>
+                    </td>
                     <td scope="row" i18n>{{ getDaysRemaining(document) }} days</td>
                     <td scope="row">
                     <div class="btn-group d-block d-sm-none">
index f9a8b97714e7618336b7076a6be5f9d7de46f4ac..18b7cb94dbc7aa2cab70d0b63edcedbf70dc73c9 100644 (file)
@@ -1,30 +1,37 @@
-<div class="preview-popup-container">
-  @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>
-    </div>
-  } @else {
-    @if (renderAsObject) {
-      @if (previewText) {
-        <div class="bg-light p-3 overflow-auto whitespace-preserve" width="100%">{{previewText}}</div>
-      } @else {
-        <object [data]="previewURL | safeUrl" width="100%" class="bg-light" [class.p-2]="!isPdf"></object>
-      }
+<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>
+<ng-template #previewContent>
+  <div class="preview-popup-container">
+    @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>
+      </div>
     } @else {
-      @if (requiresPassword) {
-        <div class="w-100 h-100 position-relative">
-          <i-bs width="2em" height="2em" class="position-absolute top-50 start-50 translate-middle" name="file-earmark-lock"></i-bs>
-        </div>
-      }
-      @if (!requiresPassword) {
-        <pdf-viewer
-          [src]="previewURL"
-          [original-size]="false"
-          [show-borders]="false"
-          [show-all]="true"
-          (error)="onError($event)">
-        </pdf-viewer>
+      @if (renderAsObject) {
+        @if (previewText) {
+          <div class="bg-light p-3 overflow-auto whitespace-preserve" width="100%">{{previewText}}</div>
+        } @else {
+          <object [data]="previewURL | safeUrl" width="100%" class="bg-light" [class.p-2]="!isPdf"></object>
+        }
+      } @else {
+        @if (requiresPassword) {
+          <div class="w-100 h-100 position-relative">
+            <i-bs width="2em" height="2em" class="position-absolute top-50 start-50 translate-middle" name="file-earmark-lock"></i-bs>
+          </div>
+        }
+        @if (!requiresPassword) {
+          <pdf-viewer
+            [src]="previewURL"
+            [original-size]="false"
+            [show-borders]="false"
+            [show-all]="true"
+            (error)="onError($event)">
+          </pdf-viewer>
+        }
       }
     }
-  }
-</div>
+  </div>
+</ng-template>
index 2b9f71cef2516dde63153d6c9a6d18e0a27b3ee9..12021fc90bb4ee49e766a13537fecab3625b7fd7 100644 (file)
@@ -1,4 +1,9 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing'
+import {
+  ComponentFixture,
+  fakeAsync,
+  TestBed,
+  tick,
+} from '@angular/core/testing'
 
 import { PreviewPopupComponent } from './preview-popup.component'
 import { By } from '@angular/platform-browser'
@@ -15,6 +20,8 @@ import {
   withInterceptorsFromDi,
 } from '@angular/common/http'
 import { of, throwError } from 'rxjs'
+import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
+import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
 
 const doc = {
   id: 10,
@@ -34,8 +41,12 @@ describe('PreviewPopupComponent', () => {
 
   beforeEach(() => {
     TestBed.configureTestingModule({
-      declarations: [PreviewPopupComponent, SafeUrlPipe],
-      imports: [NgxBootstrapIconsModule.pick(allIcons), PdfViewerModule],
+      declarations: [PreviewPopupComponent, SafeUrlPipe, DocumentTitlePipe],
+      imports: [
+        NgxBootstrapIconsModule.pick(allIcons),
+        PdfViewerModule,
+        NgbPopoverModule,
+      ],
       providers: [
         provideHttpClient(withInterceptorsFromDi()),
         provideHttpClientTesting(),
@@ -70,12 +81,14 @@ describe('PreviewPopupComponent', () => {
 
   it('should render object if native PDF viewer enabled', () => {
     settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, true)
+    component.popover.open()
     fixture.detectChanges()
     expect(fixture.debugElement.query(By.css('object'))).not.toBeNull()
   })
 
   it('should render pngx viewer if native PDF viewer disabled', () => {
     settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
+    component.popover.open()
     fixture.detectChanges()
     expect(fixture.debugElement.query(By.css('object'))).toBeNull()
     expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull()
@@ -83,6 +96,7 @@ describe('PreviewPopupComponent', () => {
 
   it('should show lock icon on password error', () => {
     settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
+    component.popover.open()
     component.onError({ name: 'PasswordException' })
     fixture.detectChanges()
     expect(component.requiresPassword).toBeTruthy()
@@ -93,16 +107,18 @@ describe('PreviewPopupComponent', () => {
     component.document.original_file_name = 'sample.png'
     component.document.mime_type = 'image/png'
     component.document.archived_file_name = undefined
+    component.popover.open()
     fixture.detectChanges()
     expect(fixture.debugElement.query(By.css('object'))).not.toBeNull()
   })
 
   it('should show message on error', () => {
+    component.popover.open()
     component.onError({})
     fixture.detectChanges()
-    expect(fixture.debugElement.nativeElement.textContent).toContain(
-      'Error loading preview'
-    )
+    expect(
+      fixture.debugElement.query(By.css('.popover')).nativeElement.textContent
+    ).toContain('Error loading preview')
   })
 
   it('should get text content from http if appropriate', () => {
@@ -122,4 +138,17 @@ describe('PreviewPopupComponent', () => {
     component.init()
     expect(component.previewText).toEqual('Preview text')
   })
+
+  it('should show preview on mouseover after delay to preload content', fakeAsync(() => {
+    component.mouseEnterPreview()
+    expect(component.popover.isOpen()).toBeTruthy()
+    tick(600)
+    component.close()
+
+    component.mouseEnterPreview()
+    tick(100)
+    component.mouseLeavePreview()
+    tick(600)
+    expect(component.popover.isOpen()).toBeFalsy()
+  }))
 })
index 6d2ede2660a53bf740fc0f7b6ecee2defc4adc3d..75f3cbb869f8e7227a104c2cad41a3d8f351020f 100644 (file)
@@ -1,5 +1,6 @@
 import { HttpClient } from '@angular/common/http'
-import { Component, Input, OnDestroy } from '@angular/core'
+import { Component, Input, OnDestroy, ViewChild } from '@angular/core'
+import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
 import { first, Subject, takeUntil } from 'rxjs'
 import { Document } from 'src/app/data/document'
 import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
@@ -23,6 +24,18 @@ export class PreviewPopupComponent implements OnDestroy {
     return this._document
   }
 
+  @Input()
+  link: string
+
+  @Input()
+  linkClasses: string = 'btn btn-sm btn-outline-secondary'
+
+  @Input()
+  linkTarget: string = '_blank'
+
+  @Input()
+  linkTitle: string = $localize`Open preview`
+
   unsubscribeNotifier: Subject<any> = new Subject()
 
   error = false
@@ -31,6 +44,12 @@ export class PreviewPopupComponent implements OnDestroy {
 
   previewText: string
 
+  @ViewChild('popover') popover: NgbPopover
+
+  mouseOnPreview: boolean
+
+  popoverClass: string = 'shadow popover-preview'
+
   get renderAsObject(): boolean {
     return (this.isPdf && this.useNativePdfViewer) || !this.isPdf
   }
@@ -83,4 +102,33 @@ export class PreviewPopupComponent implements OnDestroy {
       this.error = true
     }
   }
+
+  get previewUrl() {
+    return this.documentService.getPreviewUrl(this.document.id)
+  }
+
+  mouseEnterPreview() {
+    this.mouseOnPreview = true
+    if (!this.popover.isOpen()) {
+      // we're going to open but hide to pre-load content during hover delay
+      this.popover.open()
+      this.popoverClass = 'shadow popover-preview pe-none opacity-0'
+      setTimeout(() => {
+        if (this.mouseOnPreview) {
+          // show popover
+          this.popoverClass = this.popoverClass.replace('pe-none opacity-0', '')
+        } else {
+          this.popover.close()
+        }
+      }, 600)
+    }
+  }
+
+  mouseLeavePreview() {
+    this.mouseOnPreview = false
+  }
+
+  public close() {
+    this.popover.close(false)
+  }
 }
index 04f3a236acd3a8035c04cd219b87ec578a96c7d6..34557be311bbcd89fe6c280dd956f206ab9ea170 100644 (file)
@@ -1,4 +1,4 @@
-<div class="card mb-3 shadow-sm bg-light" [class.card-selected]="selected" [class.document-card]="selectable" [class.popover-hidden]="popoverHidden" (mouseleave)="mouseLeaveCard()">
+<div class="card mb-3 shadow-sm bg-light" [class.card-selected]="selected" [class.document-card]="selectable" (mouseleave)="mouseLeaveCard()">
   <div class="row g-0">
     <div class="col-md-2 doc-img-container rounded-start" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit()">
       <img [src]="getThumbUrl()" class="card-img doc-img border-end rounded-start" [class.inverted]="getIsThumbInverted()">
             <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
               <i-bs name="file-earmark-richtext"></i-bs>&nbsp;<span class="d-none d-md-inline" i18n>Open</span>
             </a>
-            <a class="btn btn-sm btn-outline-secondary" target="_blank" [href]="previewUrl"
-              [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle"
-              autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
+            <pngx-preview-popup [document]="document" #popupPreview>
               <i-bs name="eye"></i-bs>&nbsp;<span class="d-none d-md-inline" i18n>View</span>
-            </a>
-            <ng-template #previewContent>
-              <pngx-preview-popup [document]="document"></pngx-preview-popup>
-            </ng-template>
+            </pngx-preview-popup>
             <a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
               <i-bs name="download"></i-bs>&nbsp;<span class="d-none d-md-inline" i18n>Download</span>
               </a>
index efd5076beace73e4d2a30444cd900232fc6759a6..95b12d7ec4f1c51a157e5219e4f835b15c0be7d7 100644 (file)
@@ -1,11 +1,6 @@
 import { DatePipe } from '@angular/common'
 import { provideHttpClientTesting } from '@angular/common/http/testing'
-import {
-  ComponentFixture,
-  TestBed,
-  fakeAsync,
-  tick,
-} from '@angular/core/testing'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
 import { By } from '@angular/platform-browser'
 import { RouterTestingModule } from '@angular/router/testing'
 import {
@@ -84,21 +79,6 @@ describe('DocumentCardLargeComponent', () => {
     expect(fixture.nativeElement.textContent).toContain('8 pages')
   })
 
-  it('should show preview on mouseover after delay to preload content', fakeAsync(() => {
-    component.mouseEnterPreview()
-    expect(component.popover.isOpen()).toBeTruthy()
-    expect(component.popoverHidden).toBeTruthy()
-    tick(600)
-    expect(component.popoverHidden).toBeFalsy()
-    component.mouseLeaveCard()
-
-    component.mouseEnterPreview()
-    tick(100)
-    component.mouseLeavePreview()
-    tick(600)
-    expect(component.popover.isOpen()).toBeFalsy()
-  }))
-
   it('should trim content', () => {
     expect(component.contentTrimmed).toHaveLength(503) // includes ...
   })
index a3d57d950eca363962c534e44a448561f7c1f946..99597ca5aa83fc4b5c96ee1f5e58d305e981a53d 100644 (file)
@@ -12,9 +12,9 @@ import {
 } from 'src/app/data/document'
 import { DocumentService } from 'src/app/services/rest/document.service'
 import { SettingsService } from 'src/app/services/settings.service'
-import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
 import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
 import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
+import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component'
 
 @Component({
   selector: 'pngx-document-card-large',
@@ -65,7 +65,7 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions {
   @Output()
   clickMoreLike = new EventEmitter()
 
-  @ViewChild('popover') popover: NgbPopover
+  @ViewChild('popupPreview') popupPreview: PreviewPopupComponent
 
   mouseOnPreview = false
   popoverHidden = true
@@ -112,29 +112,8 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions {
     return this.documentService.getPreviewUrl(this.document.id)
   }
 
-  mouseEnterPreview() {
-    this.mouseOnPreview = true
-    if (!this.popover.isOpen()) {
-      // we're going to open but hide to pre-load content during hover delay
-      this.popover.open()
-      this.popoverHidden = true
-      setTimeout(() => {
-        if (this.mouseOnPreview) {
-          // show popover
-          this.popoverHidden = false
-        } else {
-          this.popover.close()
-        }
-      }, 600)
-    }
-  }
-
-  mouseLeavePreview() {
-    this.mouseOnPreview = false
-  }
-
   mouseLeaveCard() {
-    this.popover.close()
+    this.popupPreview.close()
   }
 
   get contentTrimmed() {
index 57bd6048b7728ab66e63ab5b5619b4988f3d1e15..60713ef0298e02c090a56e65a46518b80d411715 100644 (file)
@@ -1,5 +1,5 @@
 <div class="col p-2 h-100">
-  <div class="card h-100 shadow-sm document-card" [class.card-selected]="selected" [class.popover-hidden]="popoverHidden" (mouseleave)="mouseLeaveCard()">
+  <div class="card h-100 shadow-sm document-card" [class.card-selected]="selected" (mouseleave)="mouseLeaveCard()">
     <div class="border-bottom doc-img-container rounded-top" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit(this)">
       <img class="card-img doc-img" [class.inverted]="getIsThumbInverted()" [src]="getThumbUrl()">
 
           <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Open" i18n-title *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n-title>
             <i-bs name="file-earmark-richtext"></i-bs>
           </a>
-          <a [href]="previewUrl" target="_blank" class="btn btn-sm btn-outline-secondary"
-            [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle"
-            autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
+          <pngx-preview-popup [document]="document" #popupPreview>
             <i-bs name="eye"></i-bs>
-          </a>
-          <ng-template #previewContent>
-            <pngx-preview-popup [document]="document"></pngx-preview-popup>
-          </ng-template>
+          </pngx-preview-popup>
           <a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" i18n-title (click)="$event.stopPropagation()">
             <i-bs name="download"></i-bs>
           </a>
index b86453a25fe2bd83018d02fcc652a7f818e04b45..0c0c821036e4bcde038f80d43385e5bfb6b13406 100644 (file)
@@ -1,11 +1,6 @@
 import { DatePipe } from '@angular/common'
 import { provideHttpClientTesting } from '@angular/common/http/testing'
-import {
-  ComponentFixture,
-  TestBed,
-  fakeAsync,
-  tick,
-} from '@angular/core/testing'
+import { ComponentFixture, TestBed } from '@angular/core/testing'
 import { RouterTestingModule } from '@angular/router/testing'
 import {
   NgbPopoverModule,
@@ -116,19 +111,4 @@ describe('DocumentCardSmallComponent', () => {
       fixture.debugElement.queryAll(By.directive(TagComponent))
     ).toHaveLength(6)
   })
-
-  it('should show preview on mouseover after delay to preload content', fakeAsync(() => {
-    component.mouseEnterPreview()
-    expect(component.popover.isOpen()).toBeTruthy()
-    expect(component.popoverHidden).toBeTruthy()
-    tick(600)
-    expect(component.popoverHidden).toBeFalsy()
-    component.mouseLeaveCard()
-
-    component.mouseEnterPreview()
-    tick(100)
-    component.mouseLeavePreview()
-    tick(600)
-    expect(component.popover.isOpen()).toBeFalsy()
-  }))
 })
index 5cd583fb0a72d07ef420c33b4f7c2cb059edc1d7..7397159af34e0a84e1e044130947dfe360c16e4a 100644 (file)
@@ -13,9 +13,9 @@ import {
 } from 'src/app/data/document'
 import { DocumentService } from 'src/app/services/rest/document.service'
 import { SettingsService } from 'src/app/services/settings.service'
-import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
 import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
 import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
+import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component'
 
 @Component({
   selector: 'pngx-document-card-small',
@@ -61,10 +61,7 @@ export class DocumentCardSmallComponent extends ComponentWithPermissions {
 
   moreTags: number = null
 
-  @ViewChild('popover') popover: NgbPopover
-
-  mouseOnPreview = false
-  popoverHidden = true
+  @ViewChild('popupPreview') popupPreview: PreviewPopupComponent
 
   getIsThumbInverted() {
     return this.settingsService.get(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED)
@@ -78,10 +75,6 @@ export class DocumentCardSmallComponent extends ComponentWithPermissions {
     return this.documentService.getDownloadUrl(this.document.id)
   }
 
-  get previewUrl() {
-    return this.documentService.getPreviewUrl(this.document.id)
-  }
-
   get privateName() {
     return $localize`Private`
   }
@@ -100,29 +93,8 @@ export class DocumentCardSmallComponent extends ComponentWithPermissions {
     )
   }
 
-  mouseEnterPreview() {
-    this.mouseOnPreview = true
-    if (!this.popover.isOpen()) {
-      // we're going to open but hide to pre-load content during hover delay
-      this.popover.open()
-      this.popoverHidden = true
-      setTimeout(() => {
-        if (this.mouseOnPreview) {
-          // show popover
-          this.popoverHidden = false
-        } else {
-          this.popover.close()
-        }
-      }, 600)
-    }
-  }
-
-  mouseLeavePreview() {
-    this.mouseOnPreview = false
-  }
-
   mouseLeaveCard() {
-    this.popover.close()
+    this.popupPreview.close()
   }
 
   get notesEnabled(): boolean {
index 4eb9d179e3dc9972f838e18da704ede240e21ddf..ebe3536e58b6ec7fe5408d52d0aae6a030b73489 100644 (file)
                 @if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) {
                   <td width="30%">
                     @if (activeDisplayFields.includes(DisplayField.TITLE)) {
-                      <a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
+                      <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) {
index 331f6e6d873baa5a6e57a92c37c669fa0cae97ce..fe1466d587df0c60a16b30dddfcde77092ca088d 100644 (file)
@@ -564,11 +564,6 @@ table.table {
   }
 }
 
-.popover-hidden .popover {
-    opacity: 0;
-    pointer-events: none;
-}
-
 // Tour
 .tour-active .popover {
   min-width: 360px;
@@ -728,3 +723,27 @@ i-bs svg {
     vertical-align: middle;
   }
 }
+
+// fixes for buttons in preview popup
+.btn-group pngx-preview-popup:not(:last-child) {
+  // Prevent double borders when buttons are next to each other
+  > .btn {
+    margin-left: calc(#{$btn-border-width} * -1);
+  }
+  > .btn {
+    @include border-end-radius(0);
+  }
+}
+.btn-group pngx-preview-popup:not(:first-child) {
+  > .btn {
+    @include border-start-radius(0);
+  }
+}
+.btn-group pngx-preview-popup {
+  position: relative;
+  flex: 1 1 auto;
+
+  > .btn {
+    display: block;
+  }
+}
index 35fa8eafc253efcbb8406305bf6187029c21882e..367559c6d199745490f0751b683c964c1fd4da48 100644 (file)
@@ -426,7 +426,7 @@ class DocumentViewSet(
         )
 
     def file_response(self, pk, request, disposition):
-        doc = Document.objects.select_related("owner").get(id=pk)
+        doc = Document.global_objects.select_related("owner").get(id=pk)
         if request.user is not None and not has_perms_owner_aware(
             request.user,
             "view_document",