diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts index 56c3414a12e..a5306606199 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts @@ -108,11 +108,21 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { // eslint-disable-next-line @angular-eslint/prefer-signals @Input() submitBtn?: ButtonComponent; + /** Emits when a file upload is started */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref + @Output() onUploadStarted = new EventEmitter(); + /** Emits after a file has been successfully uploaded */ // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @Output() onUploadSuccess = new EventEmitter(); + /** Emits when a file upload fails */ + // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals + // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref + @Output() onUploadFailed = new EventEmitter(); + /** Emits after a file has been successfully removed */ // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals // eslint-disable-next-line @angular-eslint/prefer-output-emitter-ref @@ -196,6 +206,8 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { /** Save the attachments to the cipher */ submit = async () => { + this.onUploadStarted.emit(); + const file = this.attachmentForm.value.file; if (file === null) { this.toastService.showToast({ @@ -253,6 +265,7 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { variant: "error", message: errorMessage, }); + this.onUploadFailed.emit(); } }; diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2.component.html b/libs/vault/src/cipher-view/attachments/attachments-v2.component.html index 3b096634069..a8dc22c75ac 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2.component.html +++ b/libs/vault/src/cipher-view/attachments/attachments-v2.component.html @@ -9,7 +9,9 @@ [organizationId]="organizationId" [admin]="admin" [submitBtn]="submitBtn" + (onUploadStarted)="uploadStarted()" (onUploadSuccess)="uploadSuccessful()" + (onUploadFailed)="uploadFailed()" (onRemoveSuccess)="removalSuccessful()" > diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts index 2796cae08d0..218f5b2c6d3 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts +++ b/libs/vault/src/cipher-view/attachments/attachments-v2.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { CommonModule } from "@angular/common"; -import { Component, Inject } from "@angular/core"; +import { Component, HostListener, Inject } from "@angular/core"; import { CipherId, OrganizationId } from "@bitwarden/common/types/guid"; import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values"; @@ -52,6 +52,7 @@ export class AttachmentsV2Component { admin: boolean = false; organizationId?: OrganizationId; attachmentFormId = CipherAttachmentsComponent.attachmentFormID; + private isUploading = false; /** * Constructor for AttachmentsV2Component. @@ -82,16 +83,54 @@ export class AttachmentsV2Component { }); } + /** + * Prevent browser tab from closing/refreshing during upload. + * Shows a confirmation dialog if user tries to leave during an active upload. + * This provides additional protection beyond dialogRef.disableClose. + * Using arrow function to preserve 'this' context when used as event listener. + */ + @HostListener("window:beforeunload", ["$event"]) + private handleBeforeUnloadEvent = (event: BeforeUnloadEvent): string | undefined => { + if (this.isUploading) { + event.preventDefault(); + // The custom message is not displayed in modern browsers, but MDN docs still recommend setting it for legacy support. + const message = "Upload in progress. Are you sure you want to leave?"; + event.returnValue = message; + return message; + } + return undefined; + }; + + /** + * Called when an attachment upload is started. + * Disables closing the dialog to prevent accidental interruption. + */ + uploadStarted() { + this.isUploading = true; + this.dialogRef.disableClose = true; + } + /** * Called when an attachment is successfully uploaded. - * Closes the dialog with an 'uploaded' result. + * Re-enables dialog closing and closes the dialog with an 'uploaded' result. */ uploadSuccessful() { + this.isUploading = false; + this.dialogRef.disableClose = false; this.dialogRef.close({ action: AttachmentDialogResult.Uploaded, }); } + /** + * Called when an attachment upload fails. + * Re-enables closing the dialog. + */ + uploadFailed() { + this.isUploading = false; + this.dialogRef.disableClose = false; + } + /** * Called when an attachment is successfully removed. * Closes the dialog with a 'removed' result.