From 0af4638676610dc5cfc0dad563bd8e0d67ee5dae Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 11 Dec 2023 15:58:49 -0800 Subject: [PATCH] Improved popup preview with embedded viewer, plaintext, error handling --- src-ui/src/app/app.module.ts | 2 + .../preview-popup.component.html | 27 +++++++++ .../preview-popup.component.scss | 9 +++ .../preview-popup.component.spec.ts | 60 +++++++++++++++++++ .../preview-popup/preview-popup.component.ts | 31 ++++++++++ .../saved-view-widget.component.html | 7 ++- .../saved-view-widget.component.spec.ts | 2 + .../saved-view-widget.component.ts | 21 +++++-- .../document-card-large.component.html | 7 ++- .../document-card-large.component.spec.ts | 23 +++++++ .../document-card-large.component.ts | 16 +++-- .../document-card-small.component.html | 7 ++- .../document-card-small.component.spec.ts | 23 +++++++ .../document-card-small.component.ts | 16 +++-- .../popover-preview/popover-preview.scss | 22 ------- src-ui/src/styles.scss | 5 ++ 16 files changed, 240 insertions(+), 38 deletions(-) create mode 100644 src-ui/src/app/components/common/preview-popup/preview-popup.component.html create mode 100644 src-ui/src/app/components/common/preview-popup/preview-popup.component.scss create mode 100644 src-ui/src/app/components/common/preview-popup/preview-popup.component.spec.ts create mode 100644 src-ui/src/app/components/common/preview-popup/preview-popup.component.ts delete mode 100644 src-ui/src/app/components/document-list/popover-preview/popover-preview.scss diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 6910061d2..c3b98549a 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -107,6 +107,7 @@ import { CustomFieldsDropdownComponent } from './components/common/custom-fields import { ProfileEditDialogComponent } from './components/common/profile-edit-dialog/profile-edit-dialog.component' import { PdfViewerComponent } from './components/common/pdf-viewer/pdf-viewer.component' import { DocumentLinkComponent } from './components/common/input/document-link/document-link.component' +import { PreviewPopupComponent } from './components/common/preview-popup/preview-popup.component' import localeAf from '@angular/common/locales/af' import localeAr from '@angular/common/locales/ar' @@ -261,6 +262,7 @@ function initializeApp(settings: SettingsService) { ProfileEditDialogComponent, PdfViewerComponent, DocumentLinkComponent, + PreviewPopupComponent, ], imports: [ BrowserModule, diff --git a/src-ui/src/app/components/common/preview-popup/preview-popup.component.html b/src-ui/src/app/components/common/preview-popup/preview-popup.component.html new file mode 100644 index 000000000..e81ff9a40 --- /dev/null +++ b/src-ui/src/app/components/common/preview-popup/preview-popup.component.html @@ -0,0 +1,27 @@ +
+
+

Error loading preview

+
+ + +
{{previewText}}
+
+ + + +
+ + + +
+ + +
+
+
+
diff --git a/src-ui/src/app/components/common/preview-popup/preview-popup.component.scss b/src-ui/src/app/components/common/preview-popup/preview-popup.component.scss new file mode 100644 index 000000000..af8dc565a --- /dev/null +++ b/src-ui/src/app/components/common/preview-popup/preview-popup.component.scss @@ -0,0 +1,9 @@ +.preview-popup-container > * { + width: 30rem !important; + height: 22rem !important; + overflow-y: scroll; +} + +::ng-deep .popover.popover-preview { + max-width: 32rem; +} diff --git a/src-ui/src/app/components/common/preview-popup/preview-popup.component.spec.ts b/src-ui/src/app/components/common/preview-popup/preview-popup.component.spec.ts new file mode 100644 index 000000000..945ed1572 --- /dev/null +++ b/src-ui/src/app/components/common/preview-popup/preview-popup.component.spec.ts @@ -0,0 +1,60 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { PreviewPopupComponent } from './preview-popup.component' +import { PdfViewerComponent } from '../pdf-viewer/pdf-viewer.component' +import { By } from '@angular/platform-browser' +import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe' + +describe('PreviewPopupComponent', () => { + let component: PreviewPopupComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [PreviewPopupComponent, PdfViewerComponent, SafeUrlPipe], + }) + fixture = TestBed.createComponent(PreviewPopupComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should render object if use native PDF viewer', () => { + component.useNativePdfViewer = true + component.previewURL = 'sample.pdf' + fixture.detectChanges() + expect(fixture.debugElement.query(By.css('object'))).not.toBeNull() + }) + + it('should render pngx viewer if not use native PDF viewer', () => { + component.useNativePdfViewer = false + component.previewURL = 'sample.pdf' + fixture.detectChanges() + expect(fixture.debugElement.query(By.css('object'))).toBeNull() + expect(fixture.debugElement.query(By.css('pngx-pdf-viewer'))).not.toBeNull() + }) + + it('should render plain text if needed', () => { + component.renderAsPlainText = true + component.previewText = 'Hello world' + fixture.detectChanges() + expect(fixture.debugElement.query(By.css('object'))).toBeNull() + expect(fixture.debugElement.query(By.css('pngx-pdf-viewer'))).toBeNull() + expect(fixture.debugElement.nativeElement.textContent).toContain( + 'Hello world' + ) + }) + + it('should show lock icon on password error', () => { + component.onError({ name: 'PasswordException' }) + fixture.detectChanges() + expect(fixture.debugElement.query(By.css('svg'))).not.toBeNull() + }) + + it('should show message on error', () => { + component.onError({}) + fixture.detectChanges() + expect(fixture.debugElement.nativeElement.textContent).toContain( + 'Error loading preview' + ) + }) +}) diff --git a/src-ui/src/app/components/common/preview-popup/preview-popup.component.ts b/src-ui/src/app/components/common/preview-popup/preview-popup.component.ts new file mode 100644 index 000000000..ef34db9f2 --- /dev/null +++ b/src-ui/src/app/components/common/preview-popup/preview-popup.component.ts @@ -0,0 +1,31 @@ +import { Component, Input } from '@angular/core' + +@Component({ + selector: 'pngx-preview-popup', + templateUrl: './preview-popup.component.html', + styleUrls: ['./preview-popup.component.scss'], +}) +export class PreviewPopupComponent { + @Input() + renderAsPlainText: boolean = false + + @Input() + previewText: string + + @Input() + previewURL: string + + @Input() + useNativePdfViewer: boolean = false + + error = false + requiresPassword: boolean = false + + onError(event) { + if (event.name == 'PasswordException') { + this.requiresPassword = true + } else { + this.error = true + } + } +} diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html index 637a28a54..695936ad9 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html @@ -36,7 +36,12 @@ - + + diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts index e4a3041fc..94434673e 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts @@ -29,6 +29,7 @@ import { SavedViewWidgetComponent } from './saved-view-widget.component' import { By } from '@angular/platform-browser' import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe' import { DragDropModule } from '@angular/cdk/drag-drop' +import { PreviewPopupComponent } from 'src/app/components/common/preview-popup/preview-popup.component' const savedView: PaperlessSavedView = { id: 1, @@ -74,6 +75,7 @@ describe('SavedViewWidgetComponent', () => { CustomDatePipe, DocumentTitlePipe, SafeUrlPipe, + PreviewPopupComponent, ], providers: [ PermissionsGuard, diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts index 982aeebaa..176392574 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts @@ -22,14 +22,13 @@ import { DocumentListViewService } from 'src/app/services/document-list-view.ser import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component' import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' import { queryParamsFromFilterRules } from 'src/app/utils/query-params' +import { SettingsService } from 'src/app/services/settings.service' +import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' @Component({ selector: 'pngx-saved-view-widget', templateUrl: './saved-view-widget.component.html', - styleUrls: [ - './saved-view-widget.component.scss', - '../../../document-list/popover-preview/popover-preview.scss', - ], + styleUrls: ['./saved-view-widget.component.scss'], }) export class SavedViewWidgetComponent extends ComponentWithPermissions @@ -43,7 +42,8 @@ export class SavedViewWidgetComponent private list: DocumentListViewService, private consumerStatusService: ConsumerStatusService, public openDocumentsService: OpenDocumentsService, - public documentListViewService: DocumentListViewService + public documentListViewService: DocumentListViewService, + private settingsService: SettingsService ) { super() } @@ -113,6 +113,10 @@ export class SavedViewWidgetComponent ]) } + get useNativePdfViewer(): boolean { + return this.settingsService.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER) + } + getPreviewUrl(document: PaperlessDocument): string { return this.documentService.getPreviewUrl(document.id) } @@ -121,6 +125,13 @@ export class SavedViewWidgetComponent return this.documentService.getDownloadUrl(document.id) } + isPdf(document: PaperlessDocument) { + return ( + document.original_file_name?.endsWith('.pdf') || + document.archived_file_name?.endsWith('.pdf') + ) + } + mouseEnterPreview(doc: PaperlessDocument) { this.popover = this.popovers.get(this.documents.indexOf(doc)) this.mouseOnPreview = true diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html index ba4608d3c..1cac5b76e 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html @@ -56,7 +56,12 @@  View - + + diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.spec.ts b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.spec.ts index 7b3d0a5bb..d2971936b 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.spec.ts +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.spec.ts @@ -19,6 +19,9 @@ import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe' import { DocumentCardLargeComponent } from './document-card-large.component' import { IsNumberPipe } from 'src/app/pipes/is-number.pipe' +import { SettingsService } from 'src/app/services/settings.service' +import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component' +import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' const doc = { id: 10, @@ -40,6 +43,7 @@ const doc = { describe('DocumentCardLargeComponent', () => { let component: DocumentCardLargeComponent let fixture: ComponentFixture + let settingsService: SettingsService beforeEach(async () => { TestBed.configureTestingModule({ @@ -50,6 +54,7 @@ describe('DocumentCardLargeComponent', () => { IfPermissionsDirective, SafeUrlPipe, IsNumberPipe, + PreviewPopupComponent, ], providers: [DatePipe], imports: [ @@ -62,6 +67,7 @@ describe('DocumentCardLargeComponent', () => { }).compileComponents() fixture = TestBed.createComponent(DocumentCardLargeComponent) + settingsService = TestBed.inject(SettingsService) component = fixture.componentInstance component.document = doc fixture.detectChanges() @@ -87,6 +93,23 @@ describe('DocumentCardLargeComponent', () => { expect(component.popover.isOpen()).toBeFalsy() })) + it('should guess if file is pdf by file name', () => { + component.document.original_file_name = 'sample.pdf' + expect(component.isPdf).toBeTruthy() + component.document.archived_file_name = 'sample.pdf' + expect(component.isPdf).toBeTruthy() + component.document.archived_file_name = undefined + component.document.original_file_name = 'sample.txt' + expect(component.isPdf).toBeFalsy() + }) + + it('should return settings for native PDF viewer', () => { + settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false) + expect(component.useNativePdfViewer).toBeFalsy() + settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, true) + expect(component.useNativePdfViewer).toBeTruthy() + }) + it('should trim content', () => { expect(component.contentTrimmed).toHaveLength(503) // includes ... }) diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts index 1725e5795..31cc445ff 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts @@ -15,10 +15,7 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission @Component({ selector: 'pngx-document-card-large', templateUrl: './document-card-large.component.html', - styleUrls: [ - './document-card-large.component.scss', - '../popover-preview/popover-preview.scss', - ], + styleUrls: ['./document-card-large.component.scss'], }) export class DocumentCardLargeComponent extends ComponentWithPermissions { constructor( @@ -106,6 +103,17 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions { return this.documentService.getPreviewUrl(this.document.id) } + get useNativePdfViewer(): boolean { + return this.settingsService.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER) + } + + get isPdf(): boolean { + return ( + this.document?.original_file_name?.endsWith('.pdf') || + this.document?.archived_file_name?.endsWith('.pdf') + ) + } + mouseEnterPreview() { this.mouseOnPreview = true if (!this.popover.isOpen()) { diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index 8719f2ddf..528fd2f2b 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -94,7 +94,12 @@ - + + diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts index b4b5efe11..df333786b 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts @@ -22,6 +22,9 @@ import { By } from '@angular/platform-browser' import { TagComponent } from '../../common/tag/tag.component' import { PaperlessTag } from 'src/app/data/paperless-tag' import { IsNumberPipe } from 'src/app/pipes/is-number.pipe' +import { SettingsService } from 'src/app/services/settings.service' +import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' +import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component' const doc = { id: 10, @@ -53,6 +56,7 @@ const doc = { describe('DocumentCardSmallComponent', () => { let component: DocumentCardSmallComponent let fixture: ComponentFixture + let settingsService: SettingsService beforeEach(async () => { TestBed.configureTestingModule({ @@ -64,6 +68,7 @@ describe('DocumentCardSmallComponent', () => { SafeUrlPipe, TagComponent, IsNumberPipe, + PreviewPopupComponent, ], providers: [DatePipe], imports: [ @@ -76,6 +81,7 @@ describe('DocumentCardSmallComponent', () => { }).compileComponents() fixture = TestBed.createComponent(DocumentCardSmallComponent) + settingsService = TestBed.inject(SettingsService) component = fixture.componentInstance component.document = Object.assign({}, doc) fixture.detectChanges() @@ -105,6 +111,23 @@ describe('DocumentCardSmallComponent', () => { ).toHaveLength(6) }) + it('should guess if file is pdf by file name', () => { + component.document.original_file_name = 'sample.pdf' + expect(component.isPdf).toBeTruthy() + component.document.archived_file_name = 'sample.pdf' + expect(component.isPdf).toBeTruthy() + component.document.archived_file_name = undefined + component.document.original_file_name = 'sample.txt' + expect(component.isPdf).toBeFalsy() + }) + + it('should return settings for native PDF viewer', () => { + settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false) + expect(component.useNativePdfViewer).toBeFalsy() + settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, true) + expect(component.useNativePdfViewer).toBeTruthy() + }) + it('should show preview on mouseover after delay to preload content', fakeAsync(() => { component.mouseEnterPreview() expect(component.popover.isOpen()).toBeTruthy() diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts index 65ee5e6ad..39b3e9b8b 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts @@ -16,10 +16,7 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission @Component({ selector: 'pngx-document-card-small', templateUrl: './document-card-small.component.html', - styleUrls: [ - './document-card-small.component.scss', - '../popover-preview/popover-preview.scss', - ], + styleUrls: ['./document-card-small.component.scss'], }) export class DocumentCardSmallComponent extends ComponentWithPermissions { constructor( @@ -80,6 +77,17 @@ export class DocumentCardSmallComponent extends ComponentWithPermissions { return $localize`Private` } + get useNativePdfViewer(): boolean { + return this.settingsService.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER) + } + + get isPdf(): boolean { + return ( + this.document?.original_file_name?.endsWith('.pdf') || + this.document?.archived_file_name?.endsWith('.pdf') + ) + } + getTagsLimited$() { const limit = this.document.notes.length > 0 ? 6 : 7 return this.document.tags$.pipe( diff --git a/src-ui/src/app/components/document-list/popover-preview/popover-preview.scss b/src-ui/src/app/components/document-list/popover-preview/popover-preview.scss deleted file mode 100644 index b51e2e66b..000000000 --- a/src-ui/src/app/components/document-list/popover-preview/popover-preview.scss +++ /dev/null @@ -1,22 +0,0 @@ -::ng-deep .popover.popover-preview { - max-width: 40rem; - - .preview { - min-width: 30rem; - min-height: 18rem; - max-height: 35rem; - overflow-y: scroll; - } - - .spinner-border { - position: absolute; - top: 4rem; - left: calc(50% - 0.5rem); - z-index: 0; - } -} - -::ng-deep .popover-hidden .popover { - opacity: 0; - pointer-events: none; -} diff --git a/src-ui/src/styles.scss b/src-ui/src/styles.scss index 19898374b..e128b27fa 100644 --- a/src-ui/src/styles.scss +++ b/src-ui/src/styles.scss @@ -555,6 +555,11 @@ table.table { } } +.popover-hidden .popover { + opacity: 0; + pointer-events: none; +} + // Tour .tour-active .popover { min-width: 360px;