Allow rotate from doc details page

This commit is contained in:
shamoon 2024-03-04 23:04:23 -08:00
parent 476de1fac1
commit c2cec70c9a
7 changed files with 91 additions and 8 deletions

View File

@ -461,8 +461,12 @@ Paperless-ngx added the ability to create shareable links to files in version 2.
Paperless-ngx supports 3 basic editing operations for PDFs (these operations cannot be performed on non-PDF files): Paperless-ngx supports 3 basic editing operations for PDFs (these operations cannot be performed on non-PDF files):
- Merging documents: available when selecting multiple documents for 'bulk editing' - Merging documents: available when selecting multiple documents for 'bulk editing'
- Rotating documents: available when selecting multiple documents for 'bulk editing'. Note that rotation alters the source file. - Rotating documents: available when selecting multiple documents for 'bulk editing' and from an individual document's details page.
- Splitting documents: available from an individual documents details page - Splitting documents: available from an individual document's details page
!!! important
Note that rotation alters the Paperless-ngx original file.
## Best practices {#basic-searching} ## Best practices {#basic-searching}

View File

@ -5,17 +5,17 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="row"> <div class="row">
<div class="col-3 d-flex justify-content-end"> <div class="col-2 d-flex justify-content-end">
<button class="btn btn-secondary mt-auto" (click)="rotate(false)"> <button class="btn btn-secondary mt-auto" (click)="rotate(false)">
<i-bs name="arrow-counterclockwise"></i-bs> <i-bs name="arrow-counterclockwise"></i-bs>
</button> </button>
</div> </div>
<div class="col-6 d-flex align-items-center"> <div class="col-8 d-flex align-items-center">
@if (documentID) { @if (documentID) {
<img class="w-50 m-auto" [ngStyle]="{'transform': 'rotate('+rotation+'deg)'}" [src]="documentService.getThumbUrl(documentID)" /> <img class="w-50 m-auto" [ngStyle]="{'transform': 'rotate('+rotation+'deg)'}" [src]="documentService.getThumbUrl(documentID)" />
} }
</div> </div>
<div class="col-3 d-flex"> <div class="col-2 d-flex">
<button class="btn btn-secondary mt-auto" (click)="rotate()"> <button class="btn btn-secondary mt-auto" (click)="rotate()">
<i-bs name="arrow-clockwise"></i-bs> <i-bs name="arrow-clockwise"></i-bs>
</button> </button>
@ -31,13 +31,15 @@
} }
</div> </div>
</div> </div>
@if (showPDFNote) {
<p class="small text-muted fst-italic mt-4" i18n>Note that only PDFs will be rotated.</p> <p class="small text-muted fst-italic mt-4" i18n>Note that only PDFs will be rotated.</p>
}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled"> <button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span> <span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
</button> </button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled"> <button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled || degrees === 0">
{{btnCaption}} {{btnCaption}}
@if (!confirmButtonEnabled) { @if (!confirmButtonEnabled) {
<ngb-progressbar style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar> <ngb-progressbar style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>

View File

@ -10,6 +10,7 @@ import { DocumentService } from 'src/app/services/rest/document.service'
}) })
export class RotateConfirmDialogComponent extends ConfirmDialogComponent { export class RotateConfirmDialogComponent extends ConfirmDialogComponent {
public documentID: number public documentID: number
public showPDFNote: boolean = true
// animation is better if we dont normalize yet // animation is better if we dont normalize yet
public rotation: number = 0 public rotation: number = 0

View File

@ -54,6 +54,10 @@
<button ngbDropdownItem (click)="splitDocument()" [disabled]="contentRenderType !== ContentRenderType.PDF"> <button ngbDropdownItem (click)="splitDocument()" [disabled]="contentRenderType !== ContentRenderType.PDF">
<i-bs width="1em" height="1em" name="scissors"></i-bs>&nbsp;<span i18n>Split</span> <i-bs width="1em" height="1em" name="scissors"></i-bs>&nbsp;<span i18n>Split</span>
</button> </button>
<button ngbDropdownItem (click)="rotateDocument()" [disabled]="!userIsOwner || contentRenderType !== ContentRenderType.PDF">
<i-bs name="arrow-clockwise"></i-bs>&nbsp;<ng-container i18n>Rotate</ng-container>
</button>
</div> </div>
</div> </div>

View File

@ -75,6 +75,8 @@ import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service
import { PdfViewerComponent } from '../common/pdf-viewer/pdf-viewer.component' import { PdfViewerComponent } from '../common/pdf-viewer/pdf-viewer.component'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
import { SplitConfirmDialogComponent } from '../common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
const doc: Document = { const doc: Document = {
id: 3, id: 3,
@ -171,6 +173,8 @@ describe('DocumentDetailComponent', () => {
ShareLinksDropdownComponent, ShareLinksDropdownComponent,
CustomFieldsDropdownComponent, CustomFieldsDropdownComponent,
PdfViewerComponent, PdfViewerComponent,
SplitConfirmDialogComponent,
RotateConfirmDialogComponent,
], ],
providers: [ providers: [
DocumentTitlePipe, DocumentTitlePipe,
@ -1097,6 +1101,31 @@ describe('DocumentDetailComponent', () => {
req.flush(true) req.flush(true)
}) })
it('should support rotate', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[0]))
initNormally()
component.rotateDocument()
expect(modal).not.toBeUndefined()
modal.componentInstance.documentID = doc.id
modal.componentInstance.rotate()
modal.componentInstance.confirm()
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/bulk_edit/`
)
expect(req.request.body).toEqual({
documents: [doc.id],
method: 'rotate',
parameters: { degrees: 90 },
})
req.error(new ProgressEvent('failed'))
modal.componentInstance.confirm()
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/bulk_edit/`
)
req.flush(true)
})
function initNormally() { function initNormally() {
jest jest
.spyOn(activatedRoute, 'paramMap', 'get') .spyOn(activatedRoute, 'paramMap', 'get')

View File

@ -68,6 +68,7 @@ import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { PDFDocumentProxy } from '../common/pdf-viewer/typings' import { PDFDocumentProxy } from '../common/pdf-viewer/typings'
import { SplitConfirmDialogComponent } from '../common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component' import { SplitConfirmDialogComponent } from '../common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component'
import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component'
enum DocumentDetailNavIDs { enum DocumentDetailNavIDs {
Details = 1, Details = 1,
@ -1078,4 +1079,46 @@ export class DocumentDetailComponent
}) })
}) })
} }
rotateDocument() {
let modal = this.modalService.open(RotateConfirmDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.title = $localize`Rotate confirm`
modal.componentInstance.messageBold = $localize`This operation will permanently rotate the current document.`
modal.componentInstance.message = $localize`This will alter the original copy.`
modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.documentID = this.document.id
modal.componentInstance.showPDFNote = false
modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
modal.componentInstance.buttonsEnabled = false
this.documentsService
.bulkEdit([this.document.id], 'rotate', {
degrees: modal.componentInstance.degrees,
})
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: () => {
this.toastService.show({
content: $localize`Rotation will begin in the background. Close and re-open the document after the operation has completed to see the changes.`,
delay: 8000,
action: this.close.bind(this),
actionName: $localize`Close`,
})
modal.close()
},
error: (error) => {
if (modal) {
modal.componentInstance.buttonsEnabled = true
}
this.toastService.showError(
$localize`Error executing rotate operation`,
error
)
},
})
})
}
} }

View File

@ -91,7 +91,7 @@
<button ngbDropdownItem (click)="redoOcrSelected()" [disabled]="!userCanEditAll"> <button ngbDropdownItem (click)="redoOcrSelected()" [disabled]="!userCanEditAll">
<i-bs name="body-text"></i-bs>&nbsp;<ng-container i18n>Redo OCR</ng-container> <i-bs name="body-text"></i-bs>&nbsp;<ng-container i18n>Redo OCR</ng-container>
</button> </button>
<button ngbDropdownItem (click)="rotateSelected()" [disabled]="!userCanEditAll"> <button ngbDropdownItem (click)="rotateSelected()" [disabled]="!userOwnsAll">
<i-bs name="arrow-clockwise"></i-bs>&nbsp;<ng-container i18n>Rotate</ng-container> <i-bs name="arrow-clockwise"></i-bs>&nbsp;<ng-container i18n>Rotate</ng-container>
</button> </button>
<button ngbDropdownItem (click)="mergeSelected()" [disabled]="!userCanEditAll || list.selected.size < 2"> <button ngbDropdownItem (click)="mergeSelected()" [disabled]="!userCanEditAll || list.selected.size < 2">