mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Enhancement: Add print button (#10626)
--------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
		| @@ -54,6 +54,10 @@ | ||||
|         <i-bs width="1em" height="1em" name="arrow-counterclockwise"></i-bs> <span i18n>Reprocess</span> | ||||
|       </button> | ||||
|  | ||||
|       <button ngbDropdownItem (click)="printDocument()" [hidden]="useNativePdfViewer || isMobile"> | ||||
|         <i-bs width="1em" height="1em" name="printer"></i-bs> <span i18n>Print</span> | ||||
|       </button> | ||||
|  | ||||
|       <button ngbDropdownItem (click)="moreLike()"> | ||||
|         <i-bs width="1em" height="1em" name="diagram-3"></i-bs> <span i18n>More like this</span> | ||||
|       </button> | ||||
|   | ||||
| @@ -1415,4 +1415,151 @@ describe('DocumentDetailComponent', () => { | ||||
|       .flush('fail', { status: 500, statusText: 'Server Error' }) | ||||
|     expect(component.previewText).toContain('An error occurred loading content') | ||||
|   }) | ||||
|  | ||||
|   it('should print document successfully', fakeAsync(() => { | ||||
|     initNormally() | ||||
|  | ||||
|     const appendChildSpy = jest | ||||
|       .spyOn(document.body, 'appendChild') | ||||
|       .mockImplementation((node: Node) => node) | ||||
|     const removeChildSpy = jest | ||||
|       .spyOn(document.body, 'removeChild') | ||||
|       .mockImplementation((node: Node) => node) | ||||
|     const createObjectURLSpy = jest | ||||
|       .spyOn(URL, 'createObjectURL') | ||||
|       .mockReturnValue('blob:mock-url') | ||||
|     const revokeObjectURLSpy = jest | ||||
|       .spyOn(URL, 'revokeObjectURL') | ||||
|       .mockImplementation(() => {}) | ||||
|  | ||||
|     const mockContentWindow = { | ||||
|       focus: jest.fn(), | ||||
|       print: jest.fn(), | ||||
|       onafterprint: null, | ||||
|     } | ||||
|  | ||||
|     const mockIframe = { | ||||
|       style: {}, | ||||
|       src: '', | ||||
|       onload: null, | ||||
|       contentWindow: mockContentWindow, | ||||
|     } | ||||
|  | ||||
|     const createElementSpy = jest | ||||
|       .spyOn(document, 'createElement') | ||||
|       .mockReturnValue(mockIframe as any) | ||||
|  | ||||
|     const blob = new Blob(['test'], { type: 'application/pdf' }) | ||||
|     component.printDocument() | ||||
|  | ||||
|     const req = httpTestingController.expectOne( | ||||
|       `${environment.apiBaseUrl}documents/${doc.id}/download/` | ||||
|     ) | ||||
|     req.flush(blob) | ||||
|  | ||||
|     tick() | ||||
|  | ||||
|     expect(createElementSpy).toHaveBeenCalledWith('iframe') | ||||
|     expect(appendChildSpy).toHaveBeenCalledWith(mockIframe) | ||||
|     expect(createObjectURLSpy).toHaveBeenCalledWith(blob) | ||||
|  | ||||
|     if (mockIframe.onload) { | ||||
|       mockIframe.onload({} as any) | ||||
|     } | ||||
|  | ||||
|     expect(mockContentWindow.focus).toHaveBeenCalled() | ||||
|     expect(mockContentWindow.print).toHaveBeenCalled() | ||||
|  | ||||
|     if (mockIframe.onload) { | ||||
|       mockIframe.onload(new Event('load')) | ||||
|     } | ||||
|  | ||||
|     if (mockContentWindow.onafterprint) { | ||||
|       mockContentWindow.onafterprint(new Event('afterprint')) | ||||
|     } | ||||
|  | ||||
|     expect(removeChildSpy).toHaveBeenCalledWith(mockIframe) | ||||
|     expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url') | ||||
|  | ||||
|     createElementSpy.mockRestore() | ||||
|     appendChildSpy.mockRestore() | ||||
|     removeChildSpy.mockRestore() | ||||
|     createObjectURLSpy.mockRestore() | ||||
|     revokeObjectURLSpy.mockRestore() | ||||
|   })) | ||||
|  | ||||
|   it('should show error toast if print document fails', () => { | ||||
|     initNormally() | ||||
|     const toastSpy = jest.spyOn(toastService, 'showError') | ||||
|     component.printDocument() | ||||
|     const req = httpTestingController.expectOne( | ||||
|       `${environment.apiBaseUrl}documents/${doc.id}/download/` | ||||
|     ) | ||||
|     req.error(new ErrorEvent('failed')) | ||||
|     expect(toastSpy).toHaveBeenCalledWith( | ||||
|       'Error loading document for printing.' | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   it('should show error toast if printing throws inside iframe', fakeAsync(() => { | ||||
|     initNormally() | ||||
|  | ||||
|     const appendChildSpy = jest | ||||
|       .spyOn(document.body, 'appendChild') | ||||
|       .mockImplementation((node: Node) => node) | ||||
|     const removeChildSpy = jest | ||||
|       .spyOn(document.body, 'removeChild') | ||||
|       .mockImplementation((node: Node) => node) | ||||
|     const createObjectURLSpy = jest | ||||
|       .spyOn(URL, 'createObjectURL') | ||||
|       .mockReturnValue('blob:mock-url') | ||||
|     const revokeObjectURLSpy = jest | ||||
|       .spyOn(URL, 'revokeObjectURL') | ||||
|       .mockImplementation(() => {}) | ||||
|  | ||||
|     const toastSpy = jest.spyOn(toastService, 'showError') | ||||
|  | ||||
|     const mockContentWindow = { | ||||
|       focus: jest.fn().mockImplementation(() => { | ||||
|         throw new Error('focus failed') | ||||
|       }), | ||||
|       print: jest.fn(), | ||||
|       onafterprint: null, | ||||
|     } | ||||
|  | ||||
|     const mockIframe: any = { | ||||
|       style: {}, | ||||
|       src: '', | ||||
|       onload: null, | ||||
|       contentWindow: mockContentWindow, | ||||
|     } | ||||
|  | ||||
|     const createElementSpy = jest | ||||
|       .spyOn(document, 'createElement') | ||||
|       .mockReturnValue(mockIframe as any) | ||||
|  | ||||
|     const blob = new Blob(['test'], { type: 'application/pdf' }) | ||||
|     component.printDocument() | ||||
|  | ||||
|     const req = httpTestingController.expectOne( | ||||
|       `${environment.apiBaseUrl}documents/${doc.id}/download/` | ||||
|     ) | ||||
|     req.flush(blob) | ||||
|  | ||||
|     tick() | ||||
|  | ||||
|     if (mockIframe.onload) { | ||||
|       mockIframe.onload(new Event('load')) | ||||
|     } | ||||
|  | ||||
|     expect(toastSpy).toHaveBeenCalled() | ||||
|     expect(removeChildSpy).toHaveBeenCalledWith(mockIframe) | ||||
|     expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url') | ||||
|  | ||||
|     createElementSpy.mockRestore() | ||||
|     appendChildSpy.mockRestore() | ||||
|     removeChildSpy.mockRestore() | ||||
|     createObjectURLSpy.mockRestore() | ||||
|     revokeObjectURLSpy.mockRestore() | ||||
|   })) | ||||
| }) | ||||
|   | ||||
| @@ -291,6 +291,10 @@ export class DocumentDetailComponent | ||||
|     return this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER) | ||||
|   } | ||||
|  | ||||
|   get isMobile(): boolean { | ||||
|     return this.deviceDetectorService.isMobile() | ||||
|   } | ||||
|  | ||||
|   get archiveContentRenderType(): ContentRenderType { | ||||
|     return this.document?.archived_file_name | ||||
|       ? this.getRenderType('application/pdf') | ||||
| @@ -1419,6 +1423,44 @@ export class DocumentDetailComponent | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   printDocument() { | ||||
|     const printUrl = this.documentsService.getDownloadUrl( | ||||
|       this.document.id, | ||||
|       false | ||||
|     ) | ||||
|     this.http | ||||
|       .get(printUrl, { responseType: 'blob' }) | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe({ | ||||
|         next: (blob) => { | ||||
|           const blobUrl = URL.createObjectURL(blob) | ||||
|           const iframe = document.createElement('iframe') | ||||
|           iframe.style.display = 'none' | ||||
|           iframe.src = blobUrl | ||||
|           document.body.appendChild(iframe) | ||||
|           iframe.onload = () => { | ||||
|             try { | ||||
|               iframe.contentWindow.focus() | ||||
|               iframe.contentWindow.print() | ||||
|               iframe.contentWindow.onafterprint = () => { | ||||
|                 document.body.removeChild(iframe) | ||||
|                 URL.revokeObjectURL(blobUrl) | ||||
|               } | ||||
|             } catch (err) { | ||||
|               this.toastService.showError($localize`Print failed.`, err) | ||||
|               document.body.removeChild(iframe) | ||||
|               URL.revokeObjectURL(blobUrl) | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         error: () => { | ||||
|           this.toastService.showError( | ||||
|             $localize`Error loading document for printing.` | ||||
|           ) | ||||
|         }, | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   public openShareLinks() { | ||||
|     const modal = this.modalService.open(ShareLinksDialogComponent) | ||||
|     modal.componentInstance.documentId = this.document.id | ||||
|   | ||||
| @@ -110,6 +110,7 @@ import { | ||||
|   playFill, | ||||
|   plus, | ||||
|   plusCircle, | ||||
|   printer, | ||||
|   questionCircle, | ||||
|   scissors, | ||||
|   search, | ||||
| @@ -319,6 +320,7 @@ const icons = { | ||||
|   playFill, | ||||
|   plus, | ||||
|   plusCircle, | ||||
|   printer, | ||||
|   questionCircle, | ||||
|   scissors, | ||||
|   search, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Mattia Paletti
					Mattia Paletti