mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-12-29 13:48:09 -06:00
Add password removal confirm dialog, with options
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
@if (message) {
|
||||||
|
<p class="mb-3" [innerHTML]="message"></p>
|
||||||
|
}
|
||||||
|
<div class="btn-group mb-3" role="group">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="passwordRemoveMode"
|
||||||
|
id="removeReplace"
|
||||||
|
[(ngModel)]="updateDocument"
|
||||||
|
[value]="true"
|
||||||
|
(ngModelChange)="onUpdateDocumentChange($event)"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary btn-sm" for="removeReplace">
|
||||||
|
<i-bs name="pencil"></i-bs>
|
||||||
|
<span class="ms-2" i18n>Replace current document</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="passwordRemoveMode"
|
||||||
|
id="removeCreate"
|
||||||
|
[(ngModel)]="updateDocument"
|
||||||
|
[value]="false"
|
||||||
|
(ngModelChange)="onUpdateDocumentChange($event)"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-primary btn-sm" for="removeCreate">
|
||||||
|
<i-bs name="plus"></i-bs>
|
||||||
|
<span class="ms-2" i18n>Create new document</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
@if (!updateDocument) {
|
||||||
|
<div class="d-flex flex-column flex-md-row w-100 gap-3 align-items-center">
|
||||||
|
<div class="form-group d-flex">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="copyMetaRemove" [(ngModel)]="includeMetadata" />
|
||||||
|
<label class="form-check-label" for="copyMetaRemove" i18n> Copy metadata
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check ms-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="deleteOriginalRemove" [(ngModel)]="deleteOriginal" />
|
||||||
|
<label class="form-check-label" for="deleteOriginalRemove" i18n> Delete original</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer flex-nowrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
[class]="cancelBtnClass"
|
||||||
|
(click)="cancel()"
|
||||||
|
[disabled]="!buttonsEnabled"
|
||||||
|
>
|
||||||
|
<span class="d-inline-block" style="padding-bottom: 1px;">
|
||||||
|
{{cancelBtnCaption}}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
[class]="btnClass"
|
||||||
|
(click)="confirm()"
|
||||||
|
[disabled]="!confirmButtonEnabled || !buttonsEnabled"
|
||||||
|
>
|
||||||
|
{{btnCaption}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
import { By } from '@angular/platform-browser'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { PasswordRemovalConfirmDialogComponent } from './password-removal-confirm-dialog.component'
|
||||||
|
|
||||||
|
describe('PasswordRemovalConfirmDialogComponent', () => {
|
||||||
|
let component: PasswordRemovalConfirmDialogComponent
|
||||||
|
let fixture: ComponentFixture<PasswordRemovalConfirmDialogComponent>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
providers: [NgbActiveModal],
|
||||||
|
imports: [
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
PasswordRemovalConfirmDialogComponent,
|
||||||
|
],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PasswordRemovalConfirmDialogComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should default to replacing the document', () => {
|
||||||
|
expect(component.updateDocument).toBe(true)
|
||||||
|
expect(
|
||||||
|
fixture.debugElement.query(By.css('#removeReplace')).nativeElement.checked
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should allow creating a new document with metadata and delete toggle', () => {
|
||||||
|
component.onUpdateDocumentChange(false)
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(component.updateDocument).toBe(false)
|
||||||
|
expect(fixture.debugElement.query(By.css('#copyMetaRemove'))).not.toBeNull()
|
||||||
|
|
||||||
|
component.includeMetadata = false
|
||||||
|
component.deleteOriginal = true
|
||||||
|
component.onUpdateDocumentChange(true)
|
||||||
|
expect(component.updateDocument).toBe(true)
|
||||||
|
expect(component.includeMetadata).toBe(true)
|
||||||
|
expect(component.deleteOriginal).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should emit confirm when confirmed', () => {
|
||||||
|
let confirmed = false
|
||||||
|
component.confirmClicked.subscribe(() => (confirmed = true))
|
||||||
|
component.confirm()
|
||||||
|
expect(confirmed).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Component, Input } from '@angular/core'
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
|
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-password-removal-confirm-dialog',
|
||||||
|
templateUrl: './password-removal-confirm-dialog.component.html',
|
||||||
|
styleUrls: ['./password-removal-confirm-dialog.component.scss'],
|
||||||
|
imports: [FormsModule, NgxBootstrapIconsModule],
|
||||||
|
})
|
||||||
|
export class PasswordRemovalConfirmDialogComponent extends ConfirmDialogComponent {
|
||||||
|
updateDocument: boolean = true
|
||||||
|
includeMetadata: boolean = true
|
||||||
|
deleteOriginal: boolean = false
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
override title = $localize`Remove password protection`
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
override message =
|
||||||
|
$localize`Create an unprotected copy or replace the existing file.`
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
override btnCaption = $localize`Start`
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdateDocumentChange(updateDocument: boolean) {
|
||||||
|
this.updateDocument = updateDocument
|
||||||
|
if (this.updateDocument) {
|
||||||
|
this.deleteOriginal = false
|
||||||
|
this.includeMetadata = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,7 @@ import { SettingsService } from 'src/app/services/settings.service'
|
|||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||||
|
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
|
||||||
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
||||||
import {
|
import {
|
||||||
DocumentDetailComponent,
|
DocumentDetailComponent,
|
||||||
@@ -1210,9 +1211,17 @@ describe('DocumentDetailComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should support removing password protection from pdfs', () => {
|
it('should support removing password protection from pdfs', () => {
|
||||||
|
let modal: NgbModalRef
|
||||||
|
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||||
initNormally()
|
initNormally()
|
||||||
component.password = 'secret'
|
component.password = 'secret'
|
||||||
component.removePassword()
|
component.removePassword()
|
||||||
|
const dialog =
|
||||||
|
modal.componentInstance as PasswordRemovalConfirmDialogComponent
|
||||||
|
dialog.updateDocument = false
|
||||||
|
dialog.includeMetadata = false
|
||||||
|
dialog.deleteOriginal = true
|
||||||
|
dialog.confirm()
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||||
)
|
)
|
||||||
@@ -1221,7 +1230,9 @@ describe('DocumentDetailComponent', () => {
|
|||||||
method: 'remove_password',
|
method: 'remove_password',
|
||||||
parameters: {
|
parameters: {
|
||||||
password: 'secret',
|
password: 'secret',
|
||||||
update_document: true,
|
update_document: false,
|
||||||
|
include_metadata: false,
|
||||||
|
delete_original: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
req.flush(true)
|
req.flush(true)
|
||||||
@@ -1242,11 +1253,16 @@ describe('DocumentDetailComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle failures when removing password protection', () => {
|
it('should handle failures when removing password protection', () => {
|
||||||
|
let modal: NgbModalRef
|
||||||
|
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||||
initNormally()
|
initNormally()
|
||||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||||
component.password = 'secret'
|
component.password = 'secret'
|
||||||
|
|
||||||
component.removePassword()
|
component.removePassword()
|
||||||
|
const dialog =
|
||||||
|
modal.componentInstance as PasswordRemovalConfirmDialogComponent
|
||||||
|
dialog.confirm()
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||||
)
|
)
|
||||||
@@ -1254,6 +1270,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
|
|
||||||
expect(errorSpy).toHaveBeenCalled()
|
expect(errorSpy).toHaveBeenCalled()
|
||||||
expect(component.networkActive).toBe(false)
|
expect(component.networkActive).toBe(false)
|
||||||
|
expect(dialog.buttonsEnabled).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support keyboard shortcuts', () => {
|
it('should support keyboard shortcuts', () => {
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ import { getFilenameFromContentDisposition } from 'src/app/utils/http'
|
|||||||
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||||
import * as UTIF from 'utif'
|
import * as UTIF from 'utif'
|
||||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||||
|
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
|
||||||
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
||||||
import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
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 { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
@@ -175,6 +176,7 @@ export enum ZoomSetting {
|
|||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
PdfViewerModule,
|
PdfViewerModule,
|
||||||
TextAreaComponent,
|
TextAreaComponent,
|
||||||
|
PasswordRemovalConfirmDialogComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DocumentDetailComponent
|
export class DocumentDetailComponent
|
||||||
@@ -1435,28 +1437,53 @@ export class DocumentDetailComponent
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.networkActive = true
|
const modal = this.modalService.open(
|
||||||
this.documentsService
|
PasswordRemovalConfirmDialogComponent,
|
||||||
.bulkEdit([this.document.id], 'remove_password', {
|
{
|
||||||
password: this.password,
|
backdrop: 'static',
|
||||||
update_document: true,
|
}
|
||||||
})
|
)
|
||||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
modal.componentInstance.title = $localize`Remove password protection`
|
||||||
.subscribe({
|
modal.componentInstance.message = $localize`Create an unprotected copy or replace the existing file.`
|
||||||
next: () => {
|
modal.componentInstance.btnCaption = $localize`Start`
|
||||||
this.toastService.showInfo(
|
|
||||||
$localize`Password removal operation for "${this.document.title}" will begin in the background.`
|
modal.componentInstance.confirmClicked
|
||||||
)
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
this.networkActive = false
|
.subscribe(() => {
|
||||||
this.openDocumentService.refreshDocument(this.documentId)
|
const dialog =
|
||||||
},
|
modal.componentInstance as PasswordRemovalConfirmDialogComponent
|
||||||
error: (error) => {
|
dialog.buttonsEnabled = false
|
||||||
this.networkActive = false
|
this.networkActive = true
|
||||||
this.toastService.showError(
|
this.documentsService
|
||||||
$localize`Error executing password removal operation`,
|
.bulkEdit([this.document.id], 'remove_password', {
|
||||||
error
|
password: this.password,
|
||||||
)
|
update_document: dialog.updateDocument,
|
||||||
},
|
include_metadata: dialog.includeMetadata,
|
||||||
|
delete_original: dialog.deleteOriginal,
|
||||||
|
})
|
||||||
|
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.toastService.showInfo(
|
||||||
|
$localize`Password removal operation for "${this.document.title}" will begin in the background.`
|
||||||
|
)
|
||||||
|
this.networkActive = false
|
||||||
|
modal.close()
|
||||||
|
if (!dialog.updateDocument && dialog.deleteOriginal) {
|
||||||
|
this.openDocumentService.closeDocument(this.document)
|
||||||
|
} else if (dialog.updateDocument) {
|
||||||
|
this.openDocumentService.refreshDocument(this.documentId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
dialog.buttonsEnabled = true
|
||||||
|
this.networkActive = false
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error executing password removal operation`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user