mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Add bulk download options dropdown
This commit is contained in:
parent
812df3782a
commit
48ef8eca80
@ -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,26 +74,57 @@
|
|||||||
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
<div class="d-none d-sm-inline"> <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"> <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" />
|
||||||
</svg> <ng-container i18n>Delete</ng-container>
|
</svg> <ng-container i18n>Delete</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</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 { 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', {})
|
||||||
})
|
})
|
||||||
|
@ -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' }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user