Merge branch 'dev' into feature-permissions

This commit is contained in:
Michael Shamoon
2022-12-09 03:31:26 -08:00
10 changed files with 380 additions and 64 deletions

View File

@@ -69,7 +69,7 @@
</div>
</div>
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex">
<div class="btn-toolbar me-2">
<div class="btn-group btn-group-sm me-2">
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
@@ -85,26 +85,57 @@
<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>
</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()" [disabled]="!userCanEditAll" i18n>Redo OCR</button>
</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="applyDelete()" *ifPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll">
<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 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()" *ifPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }" [disabled]="!userOwnsAll">
<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>

View File

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

View File

@@ -28,6 +28,8 @@ import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
import { PermissionsService } from 'src/app/services/permissions.service'
import { FormControl, FormGroup } from '@angular/forms'
import { first, Subject, takeUntil } from 'rxjs'
@Component({
selector: 'app-bulk-editor',
@@ -46,6 +48,14 @@ export class BulkEditorComponent extends ComponentWithPermissions {
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,
@@ -91,16 +101,46 @@ export class BulkEditorComponent extends ComponentWithPermissions {
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) {
@@ -109,8 +149,9 @@ export class BulkEditorComponent extends ComponentWithPermissions {
}
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) => {
@@ -120,7 +161,7 @@ export class BulkEditorComponent extends ComponentWithPermissions {
modal.close()
}
},
(error) => {
error: (error) => {
if (modal) {
modal.componentInstance.buttonsEnabled = true
}
@@ -129,8 +170,8 @@ export class BulkEditorComponent extends ComponentWithPermissions {
error.error
)}`
)
}
)
},
})
}
private applySelectionData(
@@ -151,6 +192,7 @@ export class BulkEditorComponent extends ComponentWithPermissions {
openTagsDropdown() {
this.documentService
.getSelectionData(Array.from(this.list.selected))
.pipe(first())
.subscribe((s) => {
this.applySelectionData(s.selected_tags, this.tagSelectionModel)
})
@@ -159,6 +201,7 @@ export class BulkEditorComponent extends ComponentWithPermissions {
openDocumentTypeDropdown() {
this.documentService
.getSelectionData(Array.from(this.list.selected))
.pipe(first())
.subscribe((s) => {
this.applySelectionData(
s.selected_document_types,
@@ -170,6 +213,7 @@ export class BulkEditorComponent extends ComponentWithPermissions {
openCorrespondentDropdown() {
this.documentService
.getSelectionData(Array.from(this.list.selected))
.pipe(first())
.subscribe((s) => {
this.applySelectionData(
s.selected_correspondents,
@@ -181,6 +225,7 @@ export class BulkEditorComponent extends ComponentWithPermissions {
openStoragePathDropdown() {
this.documentService
.getSelectionData(Array.from(this.list.selected))
.pipe(first())
.subscribe((s) => {
this.applySelectionData(
s.selected_storage_paths,
@@ -257,12 +302,14 @@ export class BulkEditorComponent extends ComponentWithPermissions {
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),
@@ -295,11 +342,13 @@ export class BulkEditorComponent extends ComponentWithPermissions {
}
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,
@@ -331,11 +380,13 @@ export class BulkEditorComponent extends ComponentWithPermissions {
}
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,
@@ -367,11 +418,13 @@ export class BulkEditorComponent extends ComponentWithPermissions {
}
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,
@@ -389,16 +442,30 @@ export class BulkEditorComponent extends ComponentWithPermissions {
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
@@ -414,10 +481,12 @@ export class BulkEditorComponent extends ComponentWithPermissions {
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', {})
})
}
setPermissions() {

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