]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Add bulk download options dropdown 2149/head
authorMichael Shamoon <4887959+shamoon@users.noreply.github.com>
Mon, 5 Dec 2022 07:09:19 +0000 (23:09 -0800)
committerTrenton H <797416+stumpylog@users.noreply.github.com>
Fri, 9 Dec 2022 02:32:14 +0000 (18:32 -0800)
src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.scss
src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts
src-ui/src/app/services/rest/document.service.ts
src-ui/src/theme.scss

index 04069d997584f07822b32352fd1108d1190adc65..d8345fd81ad78f70dd2ae21d20144e99ea154758 100644 (file)
@@ -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">
           <div class="d-none d-sm-inline">&nbsp;<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>
+          <button ngbDropdownItem (click)="redoOcrSelected()" i18n>Redo OCR</button>
+        </div>
+      </div>
+    </div>
+
+    <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">&nbsp;<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>
-          </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 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>
-          </button>
-          <button ngbDropdownItem (click)="redoOcrSelected()" i18n>Redo OCR</button>
+          </form>
         </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>&nbsp;<ng-container i18n>Delete</ng-container>
-    </button>
+    <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>&nbsp;<ng-container i18n>Delete</ng-container>
+      </button>
+    </div>
   </div>
 </div>
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..939f2c7902a24f84fd0975f4b106cf772669f1c2 100644 (file)
@@ -0,0 +1,7 @@
+.dropdown-toggle-split {
+    --bs-border-radius: .25rem;
+}
+
+.dropdown-menu{
+    --bs-dropdown-min-width: 12rem;
+}
index 7c2ca6e55afc813e996b77383f2fefa3158ed1df..dbfb9d2f06c1c91f8e8605c40f5791ee8566af3f 100644 (file)
@@ -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', {})
+      })
   }
 }
index 8e4d8eee9173fa73ff06687b8783c05c16a046c0..4e7e97110fec081352e5c14144036dd2dfca7be4 100644 (file)
@@ -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' }
     )
   }
index 77260a88268bf10d919a996ce3e10801e2a94a26..a6273eaba3cf664e730773624c967e2fb7896b1f 100644 (file)
@@ -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 {