mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Add bulk download options dropdown
This commit is contained in:
		 Michael Shamoon
					Michael Shamoon
				
			
				
					committed by
					
						 Trenton H
						Trenton H
					
				
			
			
				
	
			
			
			 Trenton H
						Trenton H
					
				
			
						parent
						
							447a892f17
						
					
				
				
					commit
					c24dc0f792
				
			| @@ -66,7 +66,6 @@ | ||||
|   </div> | ||||
|   <div class="col-auto ms-auto mb-2 mb-xl-0 d-flex"> | ||||
|     <div class="btn-group btn-group-sm me-2"> | ||||
|  | ||||
|       <div ngbDropdown class="me-2 d-flex"> | ||||
|         <button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle> | ||||
|           <svg class="toolbaricon" fill="currentColor"> | ||||
| @@ -75,26 +74,57 @@ | ||||
|           <div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div> | ||||
|         </button> | ||||
|         <div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow"> | ||||
|           <button ngbDropdownItem [disabled]="awaitingDownload" (click)="downloadSelected()" i18n> | ||||
|             Download | ||||
|             <div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status"> | ||||
|               <span class="visually-hidden">Preparing download...</span> | ||||
|             </div> | ||||
|           </button> | ||||
|           <button ngbDropdownItem [disabled]="awaitingDownload" (click)="downloadSelected('originals')" i18n> | ||||
|             Download originals | ||||
|             <div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status"> | ||||
|               <span class="visually-hidden">Preparing download...</span> | ||||
|             </div> | ||||
|           </button> | ||||
|           <button ngbDropdownItem (click)="redoOcrSelected()" i18n>Redo OCR</button> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()"> | ||||
|       <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|       </svg> <ng-container i18n>Delete</ng-container> | ||||
|     </button> | ||||
|     <div class="btn-group btn-group-sm me-2"> | ||||
|       <button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()"> | ||||
|         <svg *ngIf="!awaitingDownload" class="toolbaricon" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#arrow-down" /> | ||||
|         </svg> | ||||
|         <div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status"> | ||||
|           <span class="visually-hidden">Preparing download...</span> | ||||
|         </div> | ||||
|         <div class="d-none d-sm-inline"> <ng-container i18n>Download</ng-container></div> | ||||
|       </button> | ||||
|       <div ngbDropdown class="me-2 d-flex btn-group" role="group"> | ||||
|         <button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle-split rounded-end" ngbDropdownToggle></button> | ||||
|         <div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow"> | ||||
| 					<form [formGroup]="downloadForm" class="px-3 py-1"> | ||||
|             <p class="mb-1" i18n>Include:</p> | ||||
|             <div class="form-group ps-3 mb-2"> | ||||
|               <div class="form-check"> | ||||
|                 <input type="checkbox" class="form-check-input" id="downloadFileType_archive" formControlName="downloadFileTypeArchive" /> | ||||
|                 <label class="form-check-label" for="downloadFileType_archive" i18n> | ||||
|                   Archived files | ||||
|                 </label> | ||||
|               </div> | ||||
|               <div class="form-check"> | ||||
|                 <input type="checkbox" class="form-check-input" id="downloadFileType_originals" formControlName="downloadFileTypeOriginals" /> | ||||
|                 <label class="form-check-label" for="downloadFileType_originals" i18n> | ||||
|                   Original files | ||||
|                 </label> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div class="form-check"> | ||||
|               <input type="checkbox" class="form-check-input" id="downloadUseFormatting" formControlName="downloadUseFormatting" /> | ||||
|               <label class="form-check-label" for="downloadUseFormatting" i18n> | ||||
|                 Use formatted filename | ||||
|               </label> | ||||
|             </div> | ||||
|           </form> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div class="btn-group btn-group-sm me-2"> | ||||
|       <button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()"> | ||||
|         <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||
|           <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||
|         </svg> <ng-container i18n>Delete</ng-container> | ||||
|       </button> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|   | ||||
| @@ -0,0 +1,7 @@ | ||||
| .dropdown-toggle-split { | ||||
|     --bs-border-radius: .25rem; | ||||
| } | ||||
|  | ||||
| .dropdown-menu{ | ||||
|     --bs-dropdown-min-width: 12rem; | ||||
| } | ||||
|   | ||||
| @@ -25,6 +25,8 @@ import { saveAs } from 'file-saver' | ||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||
| import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||
| import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' | ||||
| import { FormControl, FormGroup } from '@angular/forms' | ||||
| import { first, Subject, takeUntil } from 'rxjs' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-bulk-editor', | ||||
| @@ -43,6 +45,14 @@ export class BulkEditorComponent { | ||||
|   storagePathsSelectionModel = new FilterableDropdownSelectionModel() | ||||
|   awaitingDownload: boolean | ||||
|  | ||||
|   unsubscribeNotifier: Subject<any> = new Subject() | ||||
|  | ||||
|   downloadForm = new FormGroup({ | ||||
|     downloadFileTypeArchive: new FormControl(true), | ||||
|     downloadFileTypeOriginals: new FormControl(false), | ||||
|     downloadUseFormatting: new FormControl(false), | ||||
|   }) | ||||
|  | ||||
|   constructor( | ||||
|     private documentTypeService: DocumentTypeService, | ||||
|     private tagService: TagService, | ||||
| @@ -66,16 +76,46 @@ export class BulkEditorComponent { | ||||
|   ngOnInit() { | ||||
|     this.tagService | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((result) => (this.tags = result.results)) | ||||
|     this.correspondentService | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((result) => (this.correspondents = result.results)) | ||||
|     this.documentTypeService | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((result) => (this.documentTypes = result.results)) | ||||
|     this.storagePathService | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((result) => (this.storagePaths = result.results)) | ||||
|  | ||||
|     this.downloadForm | ||||
|       .get('downloadFileTypeArchive') | ||||
|       .valueChanges.pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe((newValue) => { | ||||
|         if (!newValue) { | ||||
|           this.downloadForm | ||||
|             .get('downloadFileTypeOriginals') | ||||
|             .patchValue(true, { emitEvent: false }) | ||||
|         } | ||||
|       }) | ||||
|     this.downloadForm | ||||
|       .get('downloadFileTypeOriginals') | ||||
|       .valueChanges.pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe((newValue) => { | ||||
|         if (!newValue) { | ||||
|           this.downloadForm | ||||
|             .get('downloadFileTypeArchive') | ||||
|             .patchValue(true, { emitEvent: false }) | ||||
|         } | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   ngOnDestroy(): void { | ||||
|     this.unsubscribeNotifier.next(this) | ||||
|     this.unsubscribeNotifier.complete() | ||||
|   } | ||||
|  | ||||
|   private executeBulkOperation(modal, method: string, args) { | ||||
| @@ -84,8 +124,9 @@ export class BulkEditorComponent { | ||||
|     } | ||||
|     this.documentService | ||||
|       .bulkEdit(Array.from(this.list.selected), method, args) | ||||
|       .subscribe( | ||||
|         (response) => { | ||||
|       .pipe(first()) | ||||
|       .subscribe({ | ||||
|         next: () => { | ||||
|           this.list.reload() | ||||
|           this.list.reduceSelectionToFilter() | ||||
|           this.list.selected.forEach((id) => { | ||||
| @@ -95,7 +136,7 @@ export class BulkEditorComponent { | ||||
|             modal.close() | ||||
|           } | ||||
|         }, | ||||
|         (error) => { | ||||
|         error: (error) => { | ||||
|           if (modal) { | ||||
|             modal.componentInstance.buttonsEnabled = true | ||||
|           } | ||||
| @@ -104,8 +145,8 @@ export class BulkEditorComponent { | ||||
|               error.error | ||||
|             )}` | ||||
|           ) | ||||
|         } | ||||
|       ) | ||||
|         }, | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   private applySelectionData( | ||||
| @@ -126,6 +167,7 @@ export class BulkEditorComponent { | ||||
|   openTagsDropdown() { | ||||
|     this.documentService | ||||
|       .getSelectionData(Array.from(this.list.selected)) | ||||
|       .pipe(first()) | ||||
|       .subscribe((s) => { | ||||
|         this.applySelectionData(s.selected_tags, this.tagSelectionModel) | ||||
|       }) | ||||
| @@ -134,6 +176,7 @@ export class BulkEditorComponent { | ||||
|   openDocumentTypeDropdown() { | ||||
|     this.documentService | ||||
|       .getSelectionData(Array.from(this.list.selected)) | ||||
|       .pipe(first()) | ||||
|       .subscribe((s) => { | ||||
|         this.applySelectionData( | ||||
|           s.selected_document_types, | ||||
| @@ -145,6 +188,7 @@ export class BulkEditorComponent { | ||||
|   openCorrespondentDropdown() { | ||||
|     this.documentService | ||||
|       .getSelectionData(Array.from(this.list.selected)) | ||||
|       .pipe(first()) | ||||
|       .subscribe((s) => { | ||||
|         this.applySelectionData( | ||||
|           s.selected_correspondents, | ||||
| @@ -156,6 +200,7 @@ export class BulkEditorComponent { | ||||
|   openStoragePathDropdown() { | ||||
|     this.documentService | ||||
|       .getSelectionData(Array.from(this.list.selected)) | ||||
|       .pipe(first()) | ||||
|       .subscribe((s) => { | ||||
|         this.applySelectionData( | ||||
|           s.selected_storage_paths, | ||||
| @@ -232,12 +277,14 @@ export class BulkEditorComponent { | ||||
|  | ||||
|       modal.componentInstance.btnClass = 'btn-warning' | ||||
|       modal.componentInstance.btnCaption = $localize`Confirm` | ||||
|       modal.componentInstance.confirmClicked.subscribe(() => { | ||||
|         this.executeBulkOperation(modal, 'modify_tags', { | ||||
|           add_tags: changedTags.itemsToAdd.map((t) => t.id), | ||||
|           remove_tags: changedTags.itemsToRemove.map((t) => t.id), | ||||
|       modal.componentInstance.confirmClicked | ||||
|         .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|         .subscribe(() => { | ||||
|           this.executeBulkOperation(modal, 'modify_tags', { | ||||
|             add_tags: changedTags.itemsToAdd.map((t) => t.id), | ||||
|             remove_tags: changedTags.itemsToRemove.map((t) => t.id), | ||||
|           }) | ||||
|         }) | ||||
|       }) | ||||
|     } else { | ||||
|       this.executeBulkOperation(null, 'modify_tags', { | ||||
|         add_tags: changedTags.itemsToAdd.map((t) => t.id), | ||||
| @@ -270,11 +317,13 @@ export class BulkEditorComponent { | ||||
|       } | ||||
|       modal.componentInstance.btnClass = 'btn-warning' | ||||
|       modal.componentInstance.btnCaption = $localize`Confirm` | ||||
|       modal.componentInstance.confirmClicked.subscribe(() => { | ||||
|         this.executeBulkOperation(modal, 'set_correspondent', { | ||||
|           correspondent: correspondent ? correspondent.id : null, | ||||
|       modal.componentInstance.confirmClicked | ||||
|         .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|         .subscribe(() => { | ||||
|           this.executeBulkOperation(modal, 'set_correspondent', { | ||||
|             correspondent: correspondent ? correspondent.id : null, | ||||
|           }) | ||||
|         }) | ||||
|       }) | ||||
|     } else { | ||||
|       this.executeBulkOperation(null, 'set_correspondent', { | ||||
|         correspondent: correspondent ? correspondent.id : null, | ||||
| @@ -306,11 +355,13 @@ export class BulkEditorComponent { | ||||
|       } | ||||
|       modal.componentInstance.btnClass = 'btn-warning' | ||||
|       modal.componentInstance.btnCaption = $localize`Confirm` | ||||
|       modal.componentInstance.confirmClicked.subscribe(() => { | ||||
|         this.executeBulkOperation(modal, 'set_document_type', { | ||||
|           document_type: documentType ? documentType.id : null, | ||||
|       modal.componentInstance.confirmClicked | ||||
|         .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|         .subscribe(() => { | ||||
|           this.executeBulkOperation(modal, 'set_document_type', { | ||||
|             document_type: documentType ? documentType.id : null, | ||||
|           }) | ||||
|         }) | ||||
|       }) | ||||
|     } else { | ||||
|       this.executeBulkOperation(null, 'set_document_type', { | ||||
|         document_type: documentType ? documentType.id : null, | ||||
| @@ -342,11 +393,13 @@ export class BulkEditorComponent { | ||||
|       } | ||||
|       modal.componentInstance.btnClass = 'btn-warning' | ||||
|       modal.componentInstance.btnCaption = $localize`Confirm` | ||||
|       modal.componentInstance.confirmClicked.subscribe(() => { | ||||
|         this.executeBulkOperation(modal, 'set_storage_path', { | ||||
|           storage_path: storagePath ? storagePath.id : null, | ||||
|       modal.componentInstance.confirmClicked | ||||
|         .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|         .subscribe(() => { | ||||
|           this.executeBulkOperation(modal, 'set_storage_path', { | ||||
|             storage_path: storagePath ? storagePath.id : null, | ||||
|           }) | ||||
|         }) | ||||
|       }) | ||||
|     } else { | ||||
|       this.executeBulkOperation(null, 'set_storage_path', { | ||||
|         storage_path: storagePath ? storagePath.id : null, | ||||
| @@ -364,16 +417,30 @@ export class BulkEditorComponent { | ||||
|     modal.componentInstance.message = $localize`This operation cannot be undone.` | ||||
|     modal.componentInstance.btnClass = 'btn-danger' | ||||
|     modal.componentInstance.btnCaption = $localize`Delete document(s)` | ||||
|     modal.componentInstance.confirmClicked.subscribe(() => { | ||||
|       modal.componentInstance.buttonsEnabled = false | ||||
|       this.executeBulkOperation(modal, 'delete', {}) | ||||
|     }) | ||||
|     modal.componentInstance.confirmClicked | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe(() => { | ||||
|         modal.componentInstance.buttonsEnabled = false | ||||
|         this.executeBulkOperation(modal, 'delete', {}) | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   downloadSelected(content = 'archive') { | ||||
|   downloadSelected() { | ||||
|     this.awaitingDownload = true | ||||
|     let downloadFileType: string = | ||||
|       this.downloadForm.get('downloadFileTypeArchive').value && | ||||
|       this.downloadForm.get('downloadFileTypeOriginals').value | ||||
|         ? 'both' | ||||
|         : this.downloadForm.get('downloadFileTypeArchive').value | ||||
|         ? 'archive' | ||||
|         : 'originals' | ||||
|     this.documentService | ||||
|       .bulkDownload(Array.from(this.list.selected), content) | ||||
|       .bulkDownload( | ||||
|         Array.from(this.list.selected), | ||||
|         downloadFileType, | ||||
|         this.downloadForm.get('downloadUseFormatting').value | ||||
|       ) | ||||
|       .pipe(first()) | ||||
|       .subscribe((result: any) => { | ||||
|         saveAs(result, 'documents.zip') | ||||
|         this.awaitingDownload = false | ||||
| @@ -389,9 +456,11 @@ export class BulkEditorComponent { | ||||
|     modal.componentInstance.message = $localize`This operation cannot be undone.` | ||||
|     modal.componentInstance.btnClass = 'btn-danger' | ||||
|     modal.componentInstance.btnCaption = $localize`Proceed` | ||||
|     modal.componentInstance.confirmClicked.subscribe(() => { | ||||
|       modal.componentInstance.buttonsEnabled = false | ||||
|       this.executeBulkOperation(modal, 'redo_ocr', {}) | ||||
|     }) | ||||
|     modal.componentInstance.confirmClicked | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe(() => { | ||||
|         modal.componentInstance.buttonsEnabled = false | ||||
|         this.executeBulkOperation(modal, 'redo_ocr', {}) | ||||
|       }) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -174,10 +174,18 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument> | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   bulkDownload(ids: number[], content = 'both') { | ||||
|   bulkDownload( | ||||
|     ids: number[], | ||||
|     content = 'both', | ||||
|     useFilenameFormatting: boolean = false | ||||
|   ) { | ||||
|     return this.http.post( | ||||
|       this.getResourceUrl(null, 'bulk_download'), | ||||
|       { documents: ids, content: content }, | ||||
|       { | ||||
|         documents: ids, | ||||
|         content: content, | ||||
|         follow_formatting: useFilenameFormatting, | ||||
|       }, | ||||
|       { responseType: 'blob' } | ||||
|     ) | ||||
|   } | ||||
|   | ||||
| @@ -203,9 +203,13 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt | ||||
|   .toast, | ||||
|   .toast .toast-header, | ||||
|   .toast .btn, | ||||
|   .toast .btn-close, { | ||||
|   .toast .btn-close { | ||||
|     color: var(--pngx-primary-text-contrast); | ||||
|   } | ||||
|  | ||||
|   .dropdown-menu { | ||||
|     --bs-dropdown-color: var(--bs-body-color); | ||||
|   } | ||||
| } | ||||
|  | ||||
| body.color-scheme-dark { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user