mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
[PM-17448] add 1 time dialog when deleting managed members for admins (#13139)
* add 1 time dialog when deleting managed members for admins * fix story * refactor to show warning for each org. Add test
This commit is contained in:
@@ -2,12 +2,17 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||||
import { Component, Inject } from "@angular/core";
|
import { Component, Inject } from "@angular/core";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { DeleteManagedMemberWarningService } from "../../services/delete-managed-member/delete-managed-member-warning.service";
|
||||||
|
|
||||||
import { BulkUserDetails } from "./bulk-status.component";
|
import { BulkUserDetails } from "./bulk-status.component";
|
||||||
|
|
||||||
type BulkDeleteDialogParams = {
|
type BulkDeleteDialogParams = {
|
||||||
@@ -31,12 +36,20 @@ export class BulkDeleteDialogComponent {
|
|||||||
@Inject(DIALOG_DATA) protected dialogParams: BulkDeleteDialogParams,
|
@Inject(DIALOG_DATA) protected dialogParams: BulkDeleteDialogParams,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
private organizationUserApiService: OrganizationUserApiService,
|
private organizationUserApiService: OrganizationUserApiService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
private deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
|
||||||
) {
|
) {
|
||||||
this.organizationId = dialogParams.organizationId;
|
this.organizationId = dialogParams.organizationId;
|
||||||
this.users = dialogParams.users;
|
this.users = dialogParams.users;
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit() {
|
||||||
|
if (
|
||||||
|
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.AccountDeprovisioning))
|
||||||
|
) {
|
||||||
|
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import {
|
|||||||
convertToSelectionView,
|
convertToSelectionView,
|
||||||
PermissionMode,
|
PermissionMode,
|
||||||
} from "../../../shared/components/access-selector";
|
} from "../../../shared/components/access-selector";
|
||||||
|
import { DeleteManagedMemberWarningService } from "../../services/delete-managed-member/delete-managed-member-warning.service";
|
||||||
|
|
||||||
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
|
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
|
||||||
import { inputEmailLimitValidator } from "./validators/input-email-limit.validator";
|
import { inputEmailLimitValidator } from "./validators/input-email-limit.validator";
|
||||||
@@ -176,6 +177,7 @@ export class MemberDialogComponent implements OnDestroy {
|
|||||||
organizationService: OrganizationService,
|
organizationService: OrganizationService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
private deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
|
||||||
) {
|
) {
|
||||||
this.organization$ = accountService.activeAccount$.pipe(
|
this.organization$ = accountService.activeAccount$.pipe(
|
||||||
switchMap((account) =>
|
switchMap((account) =>
|
||||||
@@ -639,6 +641,27 @@ export class MemberDialogComponent implements OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showWarningDialog = combineLatest([
|
||||||
|
this.organization$,
|
||||||
|
this.deleteManagedMemberWarningService.warningAcknowledged(this.params.organizationId),
|
||||||
|
this.accountDeprovisioningEnabled$,
|
||||||
|
]).pipe(
|
||||||
|
map(
|
||||||
|
([organization, acknowledged, featureFlagEnabled]) =>
|
||||||
|
featureFlagEnabled &&
|
||||||
|
organization.isOwner &&
|
||||||
|
organization.productTierType === ProductTierType.Enterprise &&
|
||||||
|
!acknowledged,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (await firstValueFrom(showWarningDialog)) {
|
||||||
|
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
|
||||||
|
if (!acknowledged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
title: {
|
title: {
|
||||||
key: "deleteOrganizationUser",
|
key: "deleteOrganizationUser",
|
||||||
@@ -667,6 +690,10 @@ export class MemberDialogComponent implements OnDestroy {
|
|||||||
title: null,
|
title: null,
|
||||||
message: this.i18nService.t("organizationUserDeleted", this.params.name),
|
message: this.i18nService.t("organizationUserDeleted", this.params.name),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (await firstValueFrom(this.accountDeprovisioningEnabled$)) {
|
||||||
|
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.params.organizationId);
|
||||||
|
}
|
||||||
this.close(MemberDialogResult.Deleted);
|
this.close(MemberDialogResult.Deleted);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ import {
|
|||||||
ResetPasswordComponent,
|
ResetPasswordComponent,
|
||||||
ResetPasswordDialogResult,
|
ResetPasswordDialogResult,
|
||||||
} from "./components/reset-password.component";
|
} from "./components/reset-password.component";
|
||||||
|
import { DeleteManagedMemberWarningService } from "./services/delete-managed-member/delete-managed-member-warning.service";
|
||||||
|
|
||||||
class MembersTableDataSource extends PeopleTableDataSource<OrganizationUserView> {
|
class MembersTableDataSource extends PeopleTableDataSource<OrganizationUserView> {
|
||||||
protected statusType = OrganizationUserStatusType;
|
protected statusType = OrganizationUserStatusType;
|
||||||
@@ -138,6 +139,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
private collectionService: CollectionService,
|
private collectionService: CollectionService,
|
||||||
private billingApiService: BillingApiServiceAbstraction,
|
private billingApiService: BillingApiServiceAbstraction,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
apiService,
|
apiService,
|
||||||
@@ -585,6 +587,23 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
}
|
}
|
||||||
|
|
||||||
async bulkDelete() {
|
async bulkDelete() {
|
||||||
|
if (this.accountDeprovisioningEnabled) {
|
||||||
|
const warningAcknowledged = await firstValueFrom(
|
||||||
|
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!warningAcknowledged &&
|
||||||
|
this.organization.isOwner &&
|
||||||
|
this.organization.productTierType === ProductTierType.Enterprise
|
||||||
|
) {
|
||||||
|
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
|
||||||
|
if (!acknowledged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.actionPromise != null) {
|
if (this.actionPromise != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -774,6 +793,23 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteUser(user: OrganizationUserView) {
|
async deleteUser(user: OrganizationUserView) {
|
||||||
|
if (this.accountDeprovisioningEnabled) {
|
||||||
|
const warningAcknowledged = await firstValueFrom(
|
||||||
|
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!warningAcknowledged &&
|
||||||
|
this.organization.isOwner &&
|
||||||
|
this.organization.productTierType === ProductTierType.Enterprise
|
||||||
|
) {
|
||||||
|
const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
|
||||||
|
if (!acknowledged) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
title: {
|
title: {
|
||||||
key: "deleteOrganizationUser",
|
key: "deleteOrganizationUser",
|
||||||
@@ -792,6 +828,10 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.accountDeprovisioningEnabled) {
|
||||||
|
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organization.id);
|
||||||
|
}
|
||||||
|
|
||||||
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
|
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
|
||||||
this.organization.id,
|
this.organization.id,
|
||||||
user.id,
|
user.id,
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import {
|
||||||
|
FakeAccountService,
|
||||||
|
FakeStateProvider,
|
||||||
|
mockAccountServiceWith,
|
||||||
|
} from "@bitwarden/common/spec";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { DeleteManagedMemberWarningService } from "./delete-managed-member-warning.service";
|
||||||
|
|
||||||
|
describe("Delete managed member warning service", () => {
|
||||||
|
const userId = Utils.newGuid() as UserId;
|
||||||
|
let accountService: FakeAccountService;
|
||||||
|
let stateProvider: FakeStateProvider;
|
||||||
|
let dialogService: MockProxy<DialogService>;
|
||||||
|
let warningService: DeleteManagedMemberWarningService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
accountService = mockAccountServiceWith(userId);
|
||||||
|
stateProvider = new FakeStateProvider(accountService);
|
||||||
|
dialogService = mock();
|
||||||
|
warningService = new DeleteManagedMemberWarningService(stateProvider, dialogService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("warningAcknowledged returns false for ids that have not acknowledged the warning", async () => {
|
||||||
|
const id = Utils.newGuid();
|
||||||
|
const acknowledged = await firstValueFrom(warningService.warningAcknowledged(id));
|
||||||
|
|
||||||
|
expect(acknowledged).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("warningAcknowledged returns true for ids that have acknowledged the warning", async () => {
|
||||||
|
const id1 = Utils.newGuid();
|
||||||
|
const id2 = Utils.newGuid();
|
||||||
|
const id3 = Utils.newGuid();
|
||||||
|
await warningService.acknowledgeWarning(id1);
|
||||||
|
await warningService.acknowledgeWarning(id3);
|
||||||
|
|
||||||
|
const acknowledged1 = await firstValueFrom(warningService.warningAcknowledged(id1));
|
||||||
|
const acknowledged2 = await firstValueFrom(warningService.warningAcknowledged(id2));
|
||||||
|
const acknowledged3 = await firstValueFrom(warningService.warningAcknowledged(id3));
|
||||||
|
|
||||||
|
expect(acknowledged1).toEqual(true);
|
||||||
|
expect(acknowledged2).toEqual(false);
|
||||||
|
expect(acknowledged3).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { map } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DELETE_MANAGED_USER_WARNING,
|
||||||
|
StateProvider,
|
||||||
|
UserKeyDefinition,
|
||||||
|
} from "@bitwarden/common/platform/state";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
export const SHOW_WARNING_KEY = new UserKeyDefinition<string[]>(
|
||||||
|
DELETE_MANAGED_USER_WARNING,
|
||||||
|
"showDeleteManagedUserWarning",
|
||||||
|
{
|
||||||
|
deserializer: (b) => b,
|
||||||
|
clearOn: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
@Injectable({ providedIn: "root" })
|
||||||
|
export class DeleteManagedMemberWarningService {
|
||||||
|
private _acknowledged = this.stateProvider.getActive(SHOW_WARNING_KEY);
|
||||||
|
private acknowledgedState$ = this._acknowledged.state$;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async acknowledgeWarning(organizationId: string) {
|
||||||
|
await this._acknowledged.update((state) => {
|
||||||
|
if (!organizationId) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
if (!state) {
|
||||||
|
return [organizationId];
|
||||||
|
} else if (!state.includes(organizationId)) {
|
||||||
|
return [...state, organizationId];
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async showWarning() {
|
||||||
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: {
|
||||||
|
key: "deleteManagedUserWarning",
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
key: "deleteManagedUserWarningDesc",
|
||||||
|
},
|
||||||
|
type: "danger",
|
||||||
|
icon: "bwi-exclamation-circle",
|
||||||
|
acceptButtonText: { key: "continue" },
|
||||||
|
cancelButtonText: { key: "cancel" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
warningAcknowledged(organizationId: string) {
|
||||||
|
return this.acknowledgedState$.pipe(
|
||||||
|
map((acknowledgedIds) => acknowledgedIds?.includes(organizationId) ?? false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10346,6 +10346,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"deleteManagedUserWarningDesc": {
|
||||||
|
"message": "This action will delete the member account including all items in their vault. This replaces the previous Remove action."
|
||||||
|
},
|
||||||
|
"deleteManagedUserWarning": {
|
||||||
|
"message": "Delete is a new action!"
|
||||||
|
},
|
||||||
"seatsRemaining": {
|
"seatsRemaining": {
|
||||||
"message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.",
|
"message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ export const ORGANIZATION_MANAGEMENT_PREFERENCES_DISK = new StateDefinition(
|
|||||||
export const AC_BANNERS_DISMISSED_DISK = new StateDefinition("acBannersDismissed", "disk", {
|
export const AC_BANNERS_DISMISSED_DISK = new StateDefinition("acBannersDismissed", "disk", {
|
||||||
web: "disk-local",
|
web: "disk-local",
|
||||||
});
|
});
|
||||||
|
export const DELETE_MANAGED_USER_WARNING = new StateDefinition(
|
||||||
|
"showDeleteManagedUserWarning",
|
||||||
|
"disk",
|
||||||
|
{
|
||||||
|
web: "disk-local",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Billing
|
// Billing
|
||||||
export const BILLING_DISK = new StateDefinition("billing", "disk");
|
export const BILLING_DISK = new StateDefinition("billing", "disk");
|
||||||
|
|||||||
Reference in New Issue
Block a user