mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-10-14 02:36:10 -05:00
Feature: add support for emailing multiple documents (#10666)
--------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||
<h4 class="modal-title" id="modal-basic-title" i18n>{
|
||||
documentIds.length,
|
||||
plural,
|
||||
=1 {Email Document} other {Email {{documentIds.length}} Documents}
|
||||
}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -22,11 +26,14 @@
|
||||
<input class="form-check-input mt-0 me-2" type="checkbox" role="switch" id="useArchiveVersion" [disabled]="!hasArchiveVersion" [(ngModel)]="useArchiveVersion">
|
||||
<label class="form-check-label w-100 text-start" for="useArchiveVersion" i18n>Use archive version</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline-primary" (click)="emailDocument()" [disabled]="loading || emailAddress.length === 0 || emailMessage.length === 0 || emailSubject.length === 0">
|
||||
<button type="submit" class="btn btn-outline-primary" (click)="emailDocuments()" [disabled]="loading || emailAddress.length === 0 || emailMessage.length === 0 || emailSubject.length === 0">
|
||||
@if (loading) {
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
}
|
||||
<ng-container i18n>Send email</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-light fst-italic small mt-2">
|
||||
<ng-container i18n>Some email servers may reject messages with large attachments.</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -36,31 +36,59 @@ describe('EmailDocumentDialogComponent', () => {
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
component = fixture.componentInstance
|
||||
component.documentIds = [1]
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should set hasArchiveVersion and useArchiveVersion', () => {
|
||||
expect(component.hasArchiveVersion).toBeTruthy()
|
||||
expect(component.useArchiveVersion).toBeTruthy()
|
||||
|
||||
component.hasArchiveVersion = false
|
||||
expect(component.hasArchiveVersion).toBeFalsy()
|
||||
expect(component.useArchiveVersion).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should support sending document via email, showing error if needed', () => {
|
||||
it('should support sending single document via email, showing error if needed', () => {
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastSuccessSpy = jest.spyOn(toastService, 'showInfo')
|
||||
component.documentIds = [1]
|
||||
component.emailAddress = 'hello@paperless-ngx.com'
|
||||
component.emailSubject = 'Hello'
|
||||
component.emailMessage = 'World'
|
||||
jest
|
||||
.spyOn(documentService, 'emailDocument')
|
||||
.spyOn(documentService, 'emailDocuments')
|
||||
.mockReturnValue(throwError(() => new Error('Unable to email document')))
|
||||
component.emailDocument()
|
||||
expect(toastErrorSpy).toHaveBeenCalled()
|
||||
component.emailDocuments()
|
||||
expect(toastErrorSpy).toHaveBeenCalledWith(
|
||||
'Error emailing document',
|
||||
expect.any(Error)
|
||||
)
|
||||
|
||||
jest.spyOn(documentService, 'emailDocument').mockReturnValue(of(true))
|
||||
component.emailDocument()
|
||||
expect(toastSuccessSpy).toHaveBeenCalled()
|
||||
jest.spyOn(documentService, 'emailDocuments').mockReturnValue(of(true))
|
||||
component.emailDocuments()
|
||||
expect(toastSuccessSpy).toHaveBeenCalledWith('Email sent')
|
||||
})
|
||||
|
||||
it('should support sending multiple documents via email, showing appropriate messages', () => {
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
const toastSuccessSpy = jest.spyOn(toastService, 'showInfo')
|
||||
component.documentIds = [1, 2, 3]
|
||||
component.emailAddress = 'hello@paperless-ngx.com'
|
||||
component.emailSubject = 'Hello'
|
||||
component.emailMessage = 'World'
|
||||
jest
|
||||
.spyOn(documentService, 'emailDocuments')
|
||||
.mockReturnValue(throwError(() => new Error('Unable to email documents')))
|
||||
component.emailDocuments()
|
||||
expect(toastErrorSpy).toHaveBeenCalledWith(
|
||||
'Error emailing documents',
|
||||
expect.any(Error)
|
||||
)
|
||||
|
||||
jest.spyOn(documentService, 'emailDocuments').mockReturnValue(of(true))
|
||||
component.emailDocuments()
|
||||
expect(toastSuccessSpy).toHaveBeenCalledWith('Email sent')
|
||||
})
|
||||
|
||||
it('should close the dialog', () => {
|
||||
|
@@ -18,10 +18,7 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission
|
||||
private toastService = inject(ToastService)
|
||||
|
||||
@Input()
|
||||
title = $localize`Email Document`
|
||||
|
||||
@Input()
|
||||
documentId: number
|
||||
documentIds: number[]
|
||||
|
||||
private _hasArchiveVersion: boolean = true
|
||||
|
||||
@@ -46,11 +43,11 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
public emailDocument() {
|
||||
public emailDocuments() {
|
||||
this.loading = true
|
||||
this.documentService
|
||||
.emailDocument(
|
||||
this.documentId,
|
||||
.emailDocuments(
|
||||
this.documentIds,
|
||||
this.emailAddress,
|
||||
this.emailSubject,
|
||||
this.emailMessage,
|
||||
@@ -67,7 +64,11 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission
|
||||
},
|
||||
error: (e) => {
|
||||
this.loading = false
|
||||
this.toastService.showError($localize`Error emailing document`, e)
|
||||
const errorMessage =
|
||||
this.documentIds.length > 1
|
||||
? $localize`Error emailing documents`
|
||||
: $localize`Error emailing document`
|
||||
this.toastService.showError(errorMessage, e)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@@ -1481,7 +1481,7 @@ export class DocumentDetailComponent
|
||||
const modal = this.modalService.open(EmailDocumentDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.documentId = this.document.id
|
||||
modal.componentInstance.documentIds = [this.document.id]
|
||||
modal.componentInstance.hasArchiveVersion =
|
||||
!!this.document?.archived_file_name
|
||||
}
|
||||
|
@@ -96,6 +96,9 @@
|
||||
<button ngbDropdownItem (click)="mergeSelected()" [disabled]="!userCanAdd || list.selected.size < 2">
|
||||
<i-bs name="journals"></i-bs> <ng-container i18n>Merge</ng-container>
|
||||
</button>
|
||||
<button ngbDropdownItem (click)="emailSelected()" [disabled]="!userCanEdit">
|
||||
<i-bs name="envelope"></i-bs> <ng-container i18n>Email</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -46,6 +46,7 @@ import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/docume
|
||||
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||
import { EmailDocumentDialogComponent } from '../../common/email-document-dialog/email-document-dialog.component'
|
||||
import {
|
||||
ChangedItems,
|
||||
FilterableDropdownComponent,
|
||||
@@ -902,4 +903,16 @@ export class BulkEditorComponent
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
emailSelected() {
|
||||
const allHaveArchiveVersion = this.list.documents
|
||||
.filter((d) => this.list.selected.has(d.id))
|
||||
.every((doc) => !!doc.archived_file_name)
|
||||
|
||||
const modal = this.modalService.open(EmailDocumentDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.documentIds = Array.from(this.list.selected)
|
||||
modal.componentInstance.hasArchiveVersion = allHaveArchiveVersion
|
||||
}
|
||||
}
|
||||
|
@@ -357,17 +357,15 @@ it('should include custom fields in sort fields if user has permission', () => {
|
||||
|
||||
it('should call appropriate api endpoint for email document', () => {
|
||||
subscription = service
|
||||
.emailDocument(
|
||||
documents[0].id,
|
||||
.emailDocuments(
|
||||
[documents[0].id],
|
||||
'hello@paperless-ngx.com',
|
||||
'hello',
|
||||
'world',
|
||||
true
|
||||
)
|
||||
.subscribe()
|
||||
httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/email/`
|
||||
)
|
||||
httpTestingController.expectOne(`${environment.apiBaseUrl}${endpoint}/email/`)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
@@ -256,14 +256,15 @@ export class DocumentService extends AbstractPaperlessService<Document> {
|
||||
return this._searchQuery
|
||||
}
|
||||
|
||||
emailDocument(
|
||||
documentId: number,
|
||||
emailDocuments(
|
||||
documentIds: number[],
|
||||
addresses: string,
|
||||
subject: string,
|
||||
message: string,
|
||||
useArchiveVersion: boolean
|
||||
): Observable<any> {
|
||||
return this.http.post(this.getResourceUrl(documentId, 'email'), {
|
||||
return this.http.post(this.getResourceUrl(null, 'email'), {
|
||||
documents: documentIds,
|
||||
addresses: addresses,
|
||||
subject: subject,
|
||||
message: message,
|
||||
|
Reference in New Issue
Block a user