[PM-23562] Prevent closing dialog and window when uploading an attachment (#17287)

* Prevent users from cancelling an in-flight upload, and attempt to block them from closing the window.

* Add comment for deprecated event.returnValue
This commit is contained in:
Nik Gilmore
2025-12-01 12:50:13 -08:00
committed by GitHub
parent aac7ca172b
commit e694ab490c
3 changed files with 56 additions and 2 deletions

View File

@@ -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<void>();
/** 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<void>();
/** 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<void>();
/** 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();
}
};

View File

@@ -9,7 +9,9 @@
[organizationId]="organizationId"
[admin]="admin"
[submitBtn]="submitBtn"
(onUploadStarted)="uploadStarted()"
(onUploadSuccess)="uploadSuccessful()"
(onUploadFailed)="uploadFailed()"
(onRemoveSuccess)="removalSuccessful()"
></app-cipher-attachments>
</ng-container>

View File

@@ -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.