Feature: PDF actions - merge, split & rotate (#6094)

This commit is contained in:
shamoon
2024-03-25 18:41:24 -07:00
committed by GitHub
parent 533b6c77a2
commit 17e53dd020
31 changed files with 1847 additions and 150 deletions

View File

@@ -80,18 +80,26 @@
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userOwnsAll || !userCanEditAll">
<i-bs name="person-fill-lock"></i-bs><div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Permissions</ng-container></div>
</button>
</button>
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
<i-bs name="three-dots"></i-bs>
<div class="d-none d-sm-inline">&nbsp;<ng-container i18n>Actions</ng-container></div>
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
<i-bs name="three-dots"></i-bs>
<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 (click)="redoOcrSelected()" [disabled]="!userCanEditAll">
<i-bs name="body-text"></i-bs>&nbsp;<ng-container i18n>Redo OCR</ng-container>
</button>
<button ngbDropdownItem (click)="rotateSelected()" [disabled]="!userOwnsAll">
<i-bs name="arrow-clockwise"></i-bs>&nbsp;<ng-container i18n>Rotate</ng-container>
</button>
<button ngbDropdownItem (click)="mergeSelected()" [disabled]="!userCanEditAll || list.selected.size < 2">
<i-bs name="journals"></i-bs>&nbsp;<ng-container i18n>Merge</ng-container>
</button>
<div ngbDropdownMenu aria-labelledby="dropdownSelect" class="shadow">
<button ngbDropdownItem (click)="redoOcrSelected()" [disabled]="!userCanEditAll" i18n>Redo OCR</button>
</div>
</div>
</div>
</div>
<div class="btn-group btn-group-sm">
<button class="btn btn-sm btn-outline-primary" [disabled]="awaitingDownload" (click)="downloadSelected()">
@@ -113,22 +121,16 @@
<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>
<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>
<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>
<label class="form-check-label" for="downloadUseFormatting" i18n>Use formatted filename</label>
</div>
</form>
</div>

View File

@@ -52,6 +52,9 @@ import { StoragePath } from 'src/app/data/storage-path'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
const selectionData: SelectionData = {
selected_tags: [
@@ -97,6 +100,9 @@ describe('BulkEditorComponent', () => {
PermissionsGroupComponent,
PermissionsUserComponent,
SwitchComponent,
RotateConfirmDialogComponent,
IsNumberPipe,
MergeConfirmDialogComponent,
],
providers: [
PermissionsService,
@@ -818,6 +824,79 @@ describe('BulkEditorComponent', () => {
) // listAllFilteredIds
})
it('should support rotate', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[0]))
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest
.spyOn(documentListViewService, 'documents', 'get')
.mockReturnValue([{ id: 3 }, { id: 4 }])
jest
.spyOn(documentListViewService, 'selected', 'get')
.mockReturnValue(new Set([3, 4]))
jest
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
.mockReturnValue(true)
fixture.detectChanges()
component.rotateSelected()
expect(modal).not.toBeUndefined()
modal.componentInstance.rotate()
modal.componentInstance.confirm()
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/bulk_edit/`
)
req.flush(true)
expect(req.request.body).toEqual({
documents: [3, 4],
method: 'rotate',
parameters: { degrees: 90 },
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
) // listAllFilteredIds
})
it('should support merge', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[0]))
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest
.spyOn(documentListViewService, 'documents', 'get')
.mockReturnValue([{ id: 3 }, { id: 4 }])
jest
.spyOn(documentService, 'getCachedMany')
.mockReturnValue(of([{ id: 3 }, { id: 4 }]))
jest
.spyOn(documentListViewService, 'selected', 'get')
.mockReturnValue(new Set([3, 4]))
jest
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
.mockReturnValue(true)
fixture.detectChanges()
component.mergeSelected()
expect(modal).not.toBeUndefined()
modal.componentInstance.metadataDocumentID = 3
modal.componentInstance.confirm()
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/bulk_edit/`
)
req.flush(true)
expect(req.request.body).toEqual({
documents: [3, 4],
method: 'merge',
parameters: { metadata_document_id: 3 },
})
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
) // listAllFilteredIds
})
it('should support bulk download with archive, originals or both and file formatting', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest

View File

@@ -6,7 +6,7 @@ import { TagService } from 'src/app/services/rest/tag.service'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import {
DocumentService,
SelectionDataItem,
@@ -39,6 +39,8 @@ import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component'
@Component({
selector: 'pngx-bulk-editor',
@@ -192,12 +194,21 @@ export class BulkEditorComponent
this.unsubscribeNotifier.complete()
}
private executeBulkOperation(modal, method: string, args) {
private executeBulkOperation(
modal: NgbModalRef,
method: string,
args: any,
overrideDocumentIDs?: number[]
) {
if (modal) {
modal.componentInstance.buttonsEnabled = false
}
this.documentService
.bulkEdit(Array.from(this.list.selected), method, args)
.bulkEdit(
overrideDocumentIDs ?? Array.from(this.list.selected),
method,
args
)
.pipe(first())
.subscribe({
next: () => {
@@ -641,4 +652,49 @@ export class BulkEditorComponent
}
)
}
rotateSelected() {
let modal = this.modalService.open(RotateConfirmDialogComponent, {
backdrop: 'static',
})
const rotateDialog = modal.componentInstance as RotateConfirmDialogComponent
rotateDialog.title = $localize`Rotate confirm`
rotateDialog.messageBold = $localize`This operation will permanently rotate ${this.list.selected.size} selected document(s).`
rotateDialog.message = $localize`This will alter the original copy.`
rotateDialog.btnClass = 'btn-danger'
rotateDialog.btnCaption = $localize`Proceed`
rotateDialog.documentID = Array.from(this.list.selected)[0]
rotateDialog.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
rotateDialog.buttonsEnabled = false
this.executeBulkOperation(modal, 'rotate', {
degrees: rotateDialog.degrees,
})
})
}
mergeSelected() {
let modal = this.modalService.open(MergeConfirmDialogComponent, {
backdrop: 'static',
})
const mergeDialog = modal.componentInstance as MergeConfirmDialogComponent
mergeDialog.title = $localize`Merge confirm`
mergeDialog.messageBold = $localize`This operation will merge ${this.list.selected.size} selected documents into a new document.`
mergeDialog.btnCaption = $localize`Proceed`
mergeDialog.documentIDs = Array.from(this.list.selected)
mergeDialog.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
const args = {}
if (mergeDialog.metadataDocumentID > -1) {
args['metadata_document_id'] = mergeDialog.metadataDocumentID
}
mergeDialog.buttonsEnabled = false
this.executeBulkOperation(modal, 'merge', args, mergeDialog.documentIDs)
this.toastService.showInfo(
$localize`Merged document will be queued for consumption.`
)
})
}
}