mirror of
https://github.com/bitwarden/browser.git
synced 2025-04-12 19:46:17 +02: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:
parent
605a5fd14b
commit
aedb899401
@ -2,12 +2,17 @@
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
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 { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { DeleteManagedMemberWarningService } from "../../services/delete-managed-member/delete-managed-member-warning.service";
|
||||
|
||||
import { BulkUserDetails } from "./bulk-status.component";
|
||||
|
||||
type BulkDeleteDialogParams = {
|
||||
@ -31,12 +36,20 @@ export class BulkDeleteDialogComponent {
|
||||
@Inject(DIALOG_DATA) protected dialogParams: BulkDeleteDialogParams,
|
||||
protected i18nService: I18nService,
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
private configService: ConfigService,
|
||||
private deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
|
||||
) {
|
||||
this.organizationId = dialogParams.organizationId;
|
||||
this.users = dialogParams.users;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (
|
||||
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.AccountDeprovisioning))
|
||||
) {
|
||||
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organizationId);
|
||||
}
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
|
@ -53,6 +53,7 @@ import {
|
||||
convertToSelectionView,
|
||||
PermissionMode,
|
||||
} 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 { inputEmailLimitValidator } from "./validators/input-email-limit.validator";
|
||||
@ -176,6 +177,7 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
organizationService: OrganizationService,
|
||||
private toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
private deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
|
||||
) {
|
||||
this.organization$ = accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
@ -639,6 +641,27 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
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({
|
||||
title: {
|
||||
key: "deleteOrganizationUser",
|
||||
@ -667,6 +690,10 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
title: null,
|
||||
message: this.i18nService.t("organizationUserDeleted", this.params.name),
|
||||
});
|
||||
|
||||
if (await firstValueFrom(this.accountDeprovisioningEnabled$)) {
|
||||
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.params.organizationId);
|
||||
}
|
||||
this.close(MemberDialogResult.Deleted);
|
||||
};
|
||||
|
||||
|
@ -81,6 +81,7 @@ import {
|
||||
ResetPasswordComponent,
|
||||
ResetPasswordDialogResult,
|
||||
} from "./components/reset-password.component";
|
||||
import { DeleteManagedMemberWarningService } from "./services/delete-managed-member/delete-managed-member-warning.service";
|
||||
|
||||
class MembersTableDataSource extends PeopleTableDataSource<OrganizationUserView> {
|
||||
protected statusType = OrganizationUserStatusType;
|
||||
@ -138,6 +139,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
private collectionService: CollectionService,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private configService: ConfigService,
|
||||
protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
@ -585,6 +587,23 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
}
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@ -774,6 +793,23 @@ export class MembersComponent extends BaseMembersComponent<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({
|
||||
title: {
|
||||
key: "deleteOrganizationUser",
|
||||
@ -792,6 +828,10 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.accountDeprovisioningEnabled) {
|
||||
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organization.id);
|
||||
}
|
||||
|
||||
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
|
||||
this.organization.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": {
|
||||
"message": "You have $REMAINING$ seats remaining out of $TOTAL$ seats assigned to this organization. Contact your provider to manage your subscription.",
|
||||
"placeholders": {
|
||||
|
@ -32,6 +32,13 @@ export const ORGANIZATION_MANAGEMENT_PREFERENCES_DISK = new StateDefinition(
|
||||
export const AC_BANNERS_DISMISSED_DISK = new StateDefinition("acBannersDismissed", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
export const DELETE_MANAGED_USER_WARNING = new StateDefinition(
|
||||
"showDeleteManagedUserWarning",
|
||||
"disk",
|
||||
{
|
||||
web: "disk-local",
|
||||
},
|
||||
);
|
||||
|
||||
// Billing
|
||||
export const BILLING_DISK = new StateDefinition("billing", "disk");
|
||||
|
Loading…
Reference in New Issue
Block a user