Add bulk download options dropdown

This commit is contained in:
Michael Shamoon 2022-12-04 23:09:19 -08:00 committed by Trenton H
parent 812df3782a
commit 48ef8eca80
5 changed files with 171 additions and 53 deletions

View File

@ -66,7 +66,6 @@
</div> </div>
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex"> <div class="col-auto ms-auto mb-2 mb-xl-0 d-flex">
<div class="btn-group btn-group-sm me-2"> <div class="btn-group btn-group-sm me-2">
<div ngbDropdown class="me-2 d-flex"> <div ngbDropdown class="me-2 d-flex">
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle> <button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
<svg class="toolbaricon" fill="currentColor"> <svg class="toolbaricon" fill="currentColor">
@ -75,22 +74,52 @@
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Actions</ng-container></div> <div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Actions</ng-container></div>
</button> </button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow"> <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> <button ngbDropdownItem (click)="redoOcrSelected()" i18n>Redo OCR</button>
</div> </div>
</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>
<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()"> <button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" /> <use xlink:href="assets/bootstrap-icons.svg#trash" />
@ -98,3 +127,4 @@
</button> </button>
</div> </div>
</div> </div>
</div>

View File

@ -0,0 +1,7 @@
.dropdown-toggle-split {
--bs-border-radius: .25rem;
}
.dropdown-menu{
--bs-dropdown-min-width: 12rem;
}

View File

@ -25,6 +25,8 @@ import { saveAs } from 'file-saver'
import { StoragePathService } from 'src/app/services/rest/storage-path.service' import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { FormControl, FormGroup } from '@angular/forms'
import { first, Subject, takeUntil } from 'rxjs'
@Component({ @Component({
selector: 'app-bulk-editor', selector: 'app-bulk-editor',
@ -43,6 +45,14 @@ export class BulkEditorComponent {
storagePathsSelectionModel = new FilterableDropdownSelectionModel() storagePathsSelectionModel = new FilterableDropdownSelectionModel()
awaitingDownload: boolean awaitingDownload: boolean
unsubscribeNotifier: Subject<any> = new Subject()
downloadForm = new FormGroup({
downloadFileTypeArchive: new FormControl(true),
downloadFileTypeOriginals: new FormControl(false),
downloadUseFormatting: new FormControl(false),
})
constructor( constructor(
private documentTypeService: DocumentTypeService, private documentTypeService: DocumentTypeService,
private tagService: TagService, private tagService: TagService,
@ -66,16 +76,46 @@ export class BulkEditorComponent {
ngOnInit() { ngOnInit() {
this.tagService this.tagService
.listAll() .listAll()
.pipe(first())
.subscribe((result) => (this.tags = result.results)) .subscribe((result) => (this.tags = result.results))
this.correspondentService this.correspondentService
.listAll() .listAll()
.pipe(first())
.subscribe((result) => (this.correspondents = result.results)) .subscribe((result) => (this.correspondents = result.results))
this.documentTypeService this.documentTypeService
.listAll() .listAll()
.pipe(first())
.subscribe((result) => (this.documentTypes = result.results)) .subscribe((result) => (this.documentTypes = result.results))
this.storagePathService this.storagePathService
.listAll() .listAll()
.pipe(first())
.subscribe((result) => (this.storagePaths = result.results)) .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) { private executeBulkOperation(modal, method: string, args) {
@ -84,8 +124,9 @@ export class BulkEditorComponent {
} }
this.documentService this.documentService
.bulkEdit(Array.from(this.list.selected), method, args) .bulkEdit(Array.from(this.list.selected), method, args)
.subscribe( .pipe(first())
(response) => { .subscribe({
next: () => {
this.list.reload() this.list.reload()
this.list.reduceSelectionToFilter() this.list.reduceSelectionToFilter()
this.list.selected.forEach((id) => { this.list.selected.forEach((id) => {
@ -95,7 +136,7 @@ export class BulkEditorComponent {
modal.close() modal.close()
} }
}, },
(error) => { error: (error) => {
if (modal) { if (modal) {
modal.componentInstance.buttonsEnabled = true modal.componentInstance.buttonsEnabled = true
} }
@ -104,8 +145,8 @@ export class BulkEditorComponent {
error.error error.error
)}` )}`
) )
} },
) })
} }
private applySelectionData( private applySelectionData(
@ -126,6 +167,7 @@ export class BulkEditorComponent {
openTagsDropdown() { openTagsDropdown() {
this.documentService this.documentService
.getSelectionData(Array.from(this.list.selected)) .getSelectionData(Array.from(this.list.selected))
.pipe(first())
.subscribe((s) => { .subscribe((s) => {
this.applySelectionData(s.selected_tags, this.tagSelectionModel) this.applySelectionData(s.selected_tags, this.tagSelectionModel)
}) })
@ -134,6 +176,7 @@ export class BulkEditorComponent {
openDocumentTypeDropdown() { openDocumentTypeDropdown() {
this.documentService this.documentService
.getSelectionData(Array.from(this.list.selected)) .getSelectionData(Array.from(this.list.selected))
.pipe(first())
.subscribe((s) => { .subscribe((s) => {
this.applySelectionData( this.applySelectionData(
s.selected_document_types, s.selected_document_types,
@ -145,6 +188,7 @@ export class BulkEditorComponent {
openCorrespondentDropdown() { openCorrespondentDropdown() {
this.documentService this.documentService
.getSelectionData(Array.from(this.list.selected)) .getSelectionData(Array.from(this.list.selected))
.pipe(first())
.subscribe((s) => { .subscribe((s) => {
this.applySelectionData( this.applySelectionData(
s.selected_correspondents, s.selected_correspondents,
@ -156,6 +200,7 @@ export class BulkEditorComponent {
openStoragePathDropdown() { openStoragePathDropdown() {
this.documentService this.documentService
.getSelectionData(Array.from(this.list.selected)) .getSelectionData(Array.from(this.list.selected))
.pipe(first())
.subscribe((s) => { .subscribe((s) => {
this.applySelectionData( this.applySelectionData(
s.selected_storage_paths, s.selected_storage_paths,
@ -232,7 +277,9 @@ export class BulkEditorComponent {
modal.componentInstance.btnClass = 'btn-warning' modal.componentInstance.btnClass = 'btn-warning'
modal.componentInstance.btnCaption = $localize`Confirm` modal.componentInstance.btnCaption = $localize`Confirm`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
this.executeBulkOperation(modal, 'modify_tags', { this.executeBulkOperation(modal, 'modify_tags', {
add_tags: changedTags.itemsToAdd.map((t) => t.id), add_tags: changedTags.itemsToAdd.map((t) => t.id),
remove_tags: changedTags.itemsToRemove.map((t) => t.id), remove_tags: changedTags.itemsToRemove.map((t) => t.id),
@ -270,7 +317,9 @@ export class BulkEditorComponent {
} }
modal.componentInstance.btnClass = 'btn-warning' modal.componentInstance.btnClass = 'btn-warning'
modal.componentInstance.btnCaption = $localize`Confirm` modal.componentInstance.btnCaption = $localize`Confirm`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
this.executeBulkOperation(modal, 'set_correspondent', { this.executeBulkOperation(modal, 'set_correspondent', {
correspondent: correspondent ? correspondent.id : null, correspondent: correspondent ? correspondent.id : null,
}) })
@ -306,7 +355,9 @@ export class BulkEditorComponent {
} }
modal.componentInstance.btnClass = 'btn-warning' modal.componentInstance.btnClass = 'btn-warning'
modal.componentInstance.btnCaption = $localize`Confirm` modal.componentInstance.btnCaption = $localize`Confirm`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
this.executeBulkOperation(modal, 'set_document_type', { this.executeBulkOperation(modal, 'set_document_type', {
document_type: documentType ? documentType.id : null, document_type: documentType ? documentType.id : null,
}) })
@ -342,7 +393,9 @@ export class BulkEditorComponent {
} }
modal.componentInstance.btnClass = 'btn-warning' modal.componentInstance.btnClass = 'btn-warning'
modal.componentInstance.btnCaption = $localize`Confirm` modal.componentInstance.btnCaption = $localize`Confirm`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
this.executeBulkOperation(modal, 'set_storage_path', { this.executeBulkOperation(modal, 'set_storage_path', {
storage_path: storagePath ? storagePath.id : null, storage_path: storagePath ? storagePath.id : null,
}) })
@ -364,16 +417,30 @@ export class BulkEditorComponent {
modal.componentInstance.message = $localize`This operation cannot be undone.` modal.componentInstance.message = $localize`This operation cannot be undone.`
modal.componentInstance.btnClass = 'btn-danger' modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Delete document(s)` modal.componentInstance.btnCaption = $localize`Delete document(s)`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled = false
this.executeBulkOperation(modal, 'delete', {}) this.executeBulkOperation(modal, 'delete', {})
}) })
} }
downloadSelected(content = 'archive') { downloadSelected() {
this.awaitingDownload = true 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 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) => { .subscribe((result: any) => {
saveAs(result, 'documents.zip') saveAs(result, 'documents.zip')
this.awaitingDownload = false this.awaitingDownload = false
@ -389,7 +456,9 @@ export class BulkEditorComponent {
modal.componentInstance.message = $localize`This operation cannot be undone.` modal.componentInstance.message = $localize`This operation cannot be undone.`
modal.componentInstance.btnClass = 'btn-danger' modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled = false
this.executeBulkOperation(modal, 'redo_ocr', {}) this.executeBulkOperation(modal, 'redo_ocr', {})
}) })

View 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( return this.http.post(
this.getResourceUrl(null, 'bulk_download'), this.getResourceUrl(null, 'bulk_download'),
{ documents: ids, content: content }, {
documents: ids,
content: content,
follow_formatting: useFilenameFormatting,
},
{ responseType: 'blob' } { responseType: 'blob' }
) )
} }

View File

@ -203,9 +203,13 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
.toast, .toast,
.toast .toast-header, .toast .toast-header,
.toast .btn, .toast .btn,
.toast .btn-close, { .toast .btn-close {
color: var(--pngx-primary-text-contrast); color: var(--pngx-primary-text-contrast);
} }
.dropdown-menu {
--bs-dropdown-color: var(--bs-body-color);
}
} }
body.color-scheme-dark { body.color-scheme-dark {