From bca409d93261402303de08e8bd62159ecbb7cbbc Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:24:35 -0800 Subject: [PATCH] Add password removal confirm dialog, with options --- ...word-removal-confirm-dialog.component.html | 75 +++++++++++++++++++ ...word-removal-confirm-dialog.component.scss | 0 ...d-removal-confirm-dialog.component.spec.ts | 53 +++++++++++++ ...ssword-removal-confirm-dialog.component.ts | 38 ++++++++++ .../document-detail.component.spec.ts | 19 ++++- .../document-detail.component.ts | 71 ++++++++++++------ 6 files changed, 233 insertions(+), 23 deletions(-) create mode 100644 src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.html create mode 100644 src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.scss create mode 100644 src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.spec.ts create mode 100644 src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.ts diff --git a/src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.html b/src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.html new file mode 100644 index 000000000..fc866fe40 --- /dev/null +++ b/src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.html @@ -0,0 +1,75 @@ + + + diff --git a/src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.scss b/src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.spec.ts b/src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.spec.ts new file mode 100644 index 000000000..a1449511b --- /dev/null +++ b/src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.spec.ts @@ -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 + + 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) + }) +}) diff --git a/src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.ts b/src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.ts new file mode 100644 index 000000000..82444ad13 --- /dev/null +++ b/src-ui/src/app/components/common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component.ts @@ -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 + } + } +} diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts index 34d6a55a3..6e2b6ef01 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts @@ -66,6 +66,7 @@ import { SettingsService } from 'src/app/services/settings.service' import { ToastService } from 'src/app/services/toast.service' import { environment } from 'src/environments/environment' 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 { DocumentDetailComponent, @@ -1210,9 +1211,17 @@ describe('DocumentDetailComponent', () => { }) it('should support removing password protection from pdfs', () => { + let modal: NgbModalRef + modalService.activeInstances.subscribe((m) => (modal = m[0])) initNormally() component.password = 'secret' component.removePassword() + const dialog = + modal.componentInstance as PasswordRemovalConfirmDialogComponent + dialog.updateDocument = false + dialog.includeMetadata = false + dialog.deleteOriginal = true + dialog.confirm() const req = httpTestingController.expectOne( `${environment.apiBaseUrl}documents/bulk_edit/` ) @@ -1221,7 +1230,9 @@ describe('DocumentDetailComponent', () => { method: 'remove_password', parameters: { password: 'secret', - update_document: true, + update_document: false, + include_metadata: false, + delete_original: true, }, }) req.flush(true) @@ -1242,11 +1253,16 @@ describe('DocumentDetailComponent', () => { }) it('should handle failures when removing password protection', () => { + let modal: NgbModalRef + modalService.activeInstances.subscribe((m) => (modal = m[0])) initNormally() const errorSpy = jest.spyOn(toastService, 'showError') component.password = 'secret' component.removePassword() + const dialog = + modal.componentInstance as PasswordRemovalConfirmDialogComponent + dialog.confirm() const req = httpTestingController.expectOne( `${environment.apiBaseUrl}documents/bulk_edit/` ) @@ -1254,6 +1270,7 @@ describe('DocumentDetailComponent', () => { expect(errorSpy).toHaveBeenCalled() expect(component.networkActive).toBe(false) + expect(dialog.buttonsEnabled).toBe(true) }) it('should support keyboard shortcuts', () => { diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 6e58a87e8..165cf0cef 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -83,6 +83,7 @@ import { getFilenameFromContentDisposition } from 'src/app/utils/http' import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter' import * as UTIF from 'utif' 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 { 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' @@ -175,6 +176,7 @@ export enum ZoomSetting { NgxBootstrapIconsModule, PdfViewerModule, TextAreaComponent, + PasswordRemovalConfirmDialogComponent, ], }) export class DocumentDetailComponent @@ -1435,28 +1437,53 @@ export class DocumentDetailComponent ) return } - this.networkActive = true - this.documentsService - .bulkEdit([this.document.id], 'remove_password', { - password: this.password, - update_document: true, - }) - .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 - this.openDocumentService.refreshDocument(this.documentId) - }, - error: (error) => { - this.networkActive = false - this.toastService.showError( - $localize`Error executing password removal operation`, - error - ) - }, + const modal = this.modalService.open( + PasswordRemovalConfirmDialogComponent, + { + backdrop: 'static', + } + ) + modal.componentInstance.title = $localize`Remove password protection` + modal.componentInstance.message = $localize`Create an unprotected copy or replace the existing file.` + modal.componentInstance.btnCaption = $localize`Start` + + modal.componentInstance.confirmClicked + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe(() => { + const dialog = + modal.componentInstance as PasswordRemovalConfirmDialogComponent + dialog.buttonsEnabled = false + this.networkActive = true + this.documentsService + .bulkEdit([this.document.id], 'remove_password', { + 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 + ) + }, + }) }) }