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> |         <i-bs width="1em" height="1em" name="arrow-counterclockwise"></i-bs> <span i18n>Reprocess</span> | ||||||
|       </button> |       </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()"> |       <button ngbDropdownItem (click)="moreLike()"> | ||||||
|         <i-bs width="1em" height="1em" name="diagram-3"></i-bs> <span i18n>More like this</span> |         <i-bs width="1em" height="1em" name="diagram-3"></i-bs> <span i18n>More like this</span> | ||||||
|       </button> |       </button> | ||||||
|   | |||||||
| @@ -1415,4 +1415,151 @@ describe('DocumentDetailComponent', () => { | |||||||
|       .flush('fail', { status: 500, statusText: 'Server Error' }) |       .flush('fail', { status: 500, statusText: 'Server Error' }) | ||||||
|     expect(component.previewText).toContain('An error occurred loading content') |     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) |     return this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   get isMobile(): boolean { | ||||||
|  |     return this.deviceDetectorService.isMobile() | ||||||
|  |   } | ||||||
|  |  | ||||||
|   get archiveContentRenderType(): ContentRenderType { |   get archiveContentRenderType(): ContentRenderType { | ||||||
|     return this.document?.archived_file_name |     return this.document?.archived_file_name | ||||||
|       ? this.getRenderType('application/pdf') |       ? 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() { |   public openShareLinks() { | ||||||
|     const modal = this.modalService.open(ShareLinksDialogComponent) |     const modal = this.modalService.open(ShareLinksDialogComponent) | ||||||
|     modal.componentInstance.documentId = this.document.id |     modal.componentInstance.documentId = this.document.id | ||||||
|   | |||||||
| @@ -110,6 +110,7 @@ import { | |||||||
|   playFill, |   playFill, | ||||||
|   plus, |   plus, | ||||||
|   plusCircle, |   plusCircle, | ||||||
|  |   printer, | ||||||
|   questionCircle, |   questionCircle, | ||||||
|   scissors, |   scissors, | ||||||
|   search, |   search, | ||||||
| @@ -319,6 +320,7 @@ const icons = { | |||||||
|   playFill, |   playFill, | ||||||
|   plus, |   plus, | ||||||
|   plusCircle, |   plusCircle, | ||||||
|  |   printer, | ||||||
|   questionCircle, |   questionCircle, | ||||||
|   scissors, |   scissors, | ||||||
|   search, |   search, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Mattia Paletti
					Mattia Paletti