From 494dd7d329adc102e34678ca59734877b2faaf9b Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 23 Feb 2026 08:17:46 -0600 Subject: [PATCH] [PM-31833] Split mark as critical and assign tasks (#18843) --- .../new-applications-dialog.component.ts | 145 ++++++++++-------- 1 file changed, 80 insertions(+), 65 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts index 5b9cea436a0..13018ba6884 100644 --- a/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/access-intelligence/activity/application-review-dialog/new-applications-dialog.component.ts @@ -9,8 +9,8 @@ import { Signal, signal, } from "@angular/core"; -import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; -import { catchError, EMPTY, from, switchMap, take } from "rxjs"; +import { toSignal } from "@angular/core/rxjs-interop"; +import { firstValueFrom } from "rxjs"; import { ApplicationHealthReportDetail, @@ -238,6 +238,12 @@ export class NewApplicationsDialogComponent { // Checks if there are selected applications and proceeds to assign tasks async handleMarkAsCritical() { + if (this.markingAsCritical()) { + return; // Prevent double-click + } + + this.markingAsCritical.set(true); + if (this.selectedApplications().size === 0) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "confirmNoSelectedCriticalApplicationsTitle" }, @@ -246,25 +252,11 @@ export class NewApplicationsDialogComponent { }); if (!confirmed) { + this.markingAsCritical.set(false); return; } } - // Skip the assign tasks view if there are no new unassigned at-risk cipher IDs - if (this.newUnassignedAtRiskCipherIds().length === 0) { - this.handleAssignTasks(); - } else { - this.currentView.set(DialogView.AssignTasks); - } - } - - // Saves the application review and assigns tasks for unassigned at-risk ciphers - protected handleAssignTasks() { - if (this.saving()) { - return; // Prevent double-click - } - this.saving.set(true); - const reviewedDate = new Date(); const updatedApplications = this.dialogParams.newApplications.map((app) => { const isCritical = this.selectedApplications().has(app.applicationName); @@ -276,56 +268,79 @@ export class NewApplicationsDialogComponent { }); // Save the application review dates and critical markings - this.dataService - .saveApplicationReviewStatus(updatedApplications) - .pipe( - takeUntilDestroyed(this.destroyRef), // Satisfy eslint rule - take(1), - switchMap(() => { - // Assign password change tasks for unassigned at-risk ciphers for critical applications - return from( - this.securityTasksService.requestPasswordChangeForCriticalApplications( - this.dialogParams.organizationId, - this.newUnassignedAtRiskCipherIds(), - ), - ); - }), - catchError((error: unknown) => { - if (error instanceof ErrorResponse && error.statusCode === 404) { - this.toastService.showToast({ - message: this.i18nService.t("mustBeOrganizationOwnerAdmin"), - variant: "error", - title: this.i18nService.t("error"), - }); + try { + await firstValueFrom(this.dataService.saveApplicationReviewStatus(updatedApplications)); - this.saving.set(false); - return EMPTY; - } - - this.logService.error( - "[NewApplicationsDialog] Failed to save application review or assign tasks", - error, - ); - this.saving.set(false); - this.toastService.showToast({ - variant: "error", - title: this.i18nService.t("errorSavingReviewStatus"), - message: this.i18nService.t("pleaseTryAgain"), - }); - - this.saving.set(false); - return EMPTY; - }), - ) - .subscribe(() => { - this.toastService.showToast({ - variant: "success", - title: this.i18nService.t("applicationReviewSaved"), - message: this.i18nService.t("newApplicationsReviewed"), - }); - this.saving.set(false); - this.handleAssigningCompleted(); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("applicationReviewSaved"), + message: this.i18nService.t("newApplicationsReviewed"), }); + + // If there are no unassigned at-risk ciphers, we can complete immediately. Otherwise, navigate to the assign tasks view. + if (this.newUnassignedAtRiskCipherIds().length === 0) { + this.handleAssigningCompleted(); + } else { + this.currentView.set(DialogView.AssignTasks); + } + } catch (error: unknown) { + this.logService.error( + "[NewApplicationsDialog] Failed to save application review status", + error, + ); + + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorSavingReviewStatus"), + message: this.i18nService.t("pleaseTryAgain"), + }); + } finally { + this.markingAsCritical.set(false); + } + } + + // Saves the application review and assigns tasks for unassigned at-risk ciphers + protected async handleAssignTasks() { + if (this.saving()) { + return; // Prevent double-click + } + this.saving.set(true); + + try { + await this.securityTasksService.requestPasswordChangeForCriticalApplications( + this.dialogParams.organizationId, + this.newUnassignedAtRiskCipherIds(), + ); + + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("success"), + message: this.i18nService.t("notifiedMembers"), + }); + + // close the dialog + this.handleAssigningCompleted(); + } catch (error: unknown) { + if (error instanceof ErrorResponse && error.statusCode === 404) { + this.toastService.showToast({ + message: this.i18nService.t("mustBeOrganizationOwnerAdmin"), + variant: "error", + title: this.i18nService.t("error"), + }); + + return; + } + + this.logService.error("[NewApplicationsDialog] Failed to assign tasks", error); + + this.toastService.showToast({ + message: this.i18nService.t("unexpectedError"), + variant: "error", + title: this.i18nService.t("error"), + }); + } finally { + this.saving.set(false); + } } /**