1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

[PM-28252] Increase Bulk Reinvite limit for cloud (#17587)

* Add IncreaseBulkReinviteLimitForCloud feature flag

* Enhance PeopleTableDataSource with bulk operation limits and feature flag integration

- Introduced a new feature flag to increase the bulk reinvite limit for cloud environments.
- Added an observable to determine if the increased limit is enabled based on the feature flag and environment.
- Updated the logic for enforcing checked user limits in bulk operations, allowing for a maximum of 4000 users when the feature flag is active.
- Refactored the constructor to initialize the new observable and manage the maximum allowed checked count dynamically.

* Add unit tests for PeopleTableDataSource to validate user limit enforcement and status counts

* Refactor MembersComponent to integrate increased bulk limit feature

- Added support for conditional user limit enforcement in bulk operations based on a feature flag.
- Introduced new dependencies for ConfigService and EnvironmentService to manage configuration settings.
- Updated methods to utilize the new getCheckedUsers function, which enforces limits when the feature is enabled.
- Refactored data source initialization to accommodate the new logic for handling checked users.

* Refactor enforceCheckedUserLimit method in PeopleTableDataSource to use filtered data for user limit enforcement and to keep checked the top rows.
Removed unnecessary comments and improved readability.

* Add bulk reinvite success messages to localization files

This update introduces new localization keys for bulk reinvite success notifications, including a general success message and a limited success message that provides details on the number of users re-invited and those excluded due to limits. This enhances user feedback during bulk operations.

* Enhance bulk reinvite functionality with toast notifications

This update modifies the MembersComponent to display success messages via toast notifications when the feature flag for increased bulk limits is enabled. If the limit is exceeded, a detailed message is shown, otherwise a general success message is displayed. The legacy dialog is retained for cases when the feature flag is disabled, ensuring consistent user feedback during bulk reinvite operations.

* Rename MaxBulkReinviteCount to CloudBulkReinviteLimit

* Refactor user retrieval logic in MembersComponent to conditionally enforce bulk limits

This update modifies the MembersComponent in both the admin console and provider management sections to replace the direct calls to getCheckedUsers() with a conditional check for increased bulk limit feature. If enabled, it enforces user limits; otherwise, it retrieves all checked users. The deprecated getCheckedUsers method has been removed to streamline the code.

* Add constructor to MembersTableDataSource for improved dependency injection

This update introduces a constructor to the MembersTableDataSource class in both the admin console and provider management sections, allowing for better dependency injection of ConfigService, EnvironmentService, and DestroyRef. This change enhances the overall structure and maintainability of the code.

* Refactor PeopleTableDataSource and MembersComponent to implement new bulk limit logic

This update modifies the PeopleTableDataSource to introduce a new property for managing increased bulk limits and refactors the MembersComponent to utilize this logic. The enforcement of user limits during bulk operations is now conditional based on the feature flag, allowing for a more flexible handling of user selections. Additionally, the method for limiting and unchecking excess users has been updated for clarity and efficiency.

* Refactor PeopleTableDataSource tests to update limit enforcement logic

This update modifies the test cases for the PeopleTableDataSource to reflect the new method for limiting and unchecking excess users. The method name has been changed from `enforceCheckedUserLimit` to `limitAndUncheckExcess`, and the tests have been adjusted accordingly to ensure they accurately validate the new functionality. Additionally, unnecessary tests have been removed to streamline the test suite.

* Change CloudBulkReinviteLimit back to 4000

* Refactor MembersComponent to utilize new getCheckedUsersInVisibleOrder method

This update modifies the MembersComponent to conditionally retrieve checked users based on the increased bulk limit feature. If enabled, it uses the new getCheckedUsersInVisibleOrder method to maintain visual consistency in the filtered/sorted table view. This change enhances the handling of user selections during bulk operations.

* Refactor PeopleTableDataSource to use Signals for increased bulk limit feature and update related tests. Removed unused imports and dependencies on DestroyRef in MembersTableDataSource components.

* Refactor MembersComponent to remove unused Signal for increased bulk limit and directly utilize dataSource method for feature flag checks.

* Implement getCheckedUsersWithLimit method to streamline user retrieval based on feature flag; update MembersComponent to utilize this new method for bulk actions.
This commit is contained in:
Rui Tomé
2025-12-08 15:30:29 +00:00
committed by GitHub
parent 8594ce3c99
commit a0943c3100
6 changed files with 399 additions and 52 deletions

View File

@@ -0,0 +1,130 @@
import { TestBed } from "@angular/core/testing";
import { ReplaySubject } from "rxjs";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import {
Environment,
EnvironmentService,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { PeopleTableDataSource } from "./people-table-data-source";
interface MockUser {
id: string;
name: string;
email: string;
status: OrganizationUserStatusType;
checked?: boolean;
}
class TestPeopleTableDataSource extends PeopleTableDataSource<any> {
protected statusType = OrganizationUserStatusType;
}
describe("PeopleTableDataSource", () => {
let dataSource: TestPeopleTableDataSource;
const createMockUser = (id: string, checked: boolean = false): MockUser => ({
id,
name: `User ${id}`,
email: `user${id}@example.com`,
status: OrganizationUserStatusType.Confirmed,
checked,
});
const createMockUsers = (count: number, checked: boolean = false): MockUser[] => {
return Array.from({ length: count }, (_, i) => createMockUser(`${i + 1}`, checked));
};
beforeEach(() => {
const featureFlagSubject = new ReplaySubject<boolean>(1);
featureFlagSubject.next(false);
const environmentSubject = new ReplaySubject<Environment>(1);
environmentSubject.next({
isCloud: () => false,
} as Environment);
const mockConfigService = {
getFeatureFlag$: jest.fn(() => featureFlagSubject.asObservable()),
} as any;
const mockEnvironmentService = {
environment$: environmentSubject.asObservable(),
} as any;
TestBed.configureTestingModule({
providers: [
{ provide: ConfigService, useValue: mockConfigService },
{ provide: EnvironmentService, useValue: mockEnvironmentService },
],
});
dataSource = TestBed.runInInjectionContext(
() => new TestPeopleTableDataSource(mockConfigService, mockEnvironmentService),
);
});
describe("limitAndUncheckExcess", () => {
it("should return all users when under limit", () => {
const users = createMockUsers(10, true);
dataSource.data = users;
const result = dataSource.limitAndUncheckExcess(users, 500);
expect(result).toHaveLength(10);
expect(result).toEqual(users);
expect(users.every((u) => u.checked)).toBe(true);
});
it("should limit users and uncheck excess", () => {
const users = createMockUsers(600, true);
dataSource.data = users;
const result = dataSource.limitAndUncheckExcess(users, 500);
expect(result).toHaveLength(500);
expect(result).toEqual(users.slice(0, 500));
expect(users.slice(0, 500).every((u) => u.checked)).toBe(true);
expect(users.slice(500).every((u) => u.checked)).toBe(false);
});
it("should only affect users in the provided array", () => {
const allUsers = createMockUsers(1000, true);
dataSource.data = allUsers;
// Pass only a subset (simulates filtering by status)
const subset = allUsers.slice(0, 600);
const result = dataSource.limitAndUncheckExcess(subset, 500);
expect(result).toHaveLength(500);
expect(subset.slice(0, 500).every((u) => u.checked)).toBe(true);
expect(subset.slice(500).every((u) => u.checked)).toBe(false);
// Users outside subset remain checked
expect(allUsers.slice(600).every((u) => u.checked)).toBe(true);
});
});
describe("status counts", () => {
it("should correctly count users by status", () => {
const users: MockUser[] = [
{ ...createMockUser("1"), status: OrganizationUserStatusType.Invited },
{ ...createMockUser("2"), status: OrganizationUserStatusType.Invited },
{ ...createMockUser("3"), status: OrganizationUserStatusType.Accepted },
{ ...createMockUser("4"), status: OrganizationUserStatusType.Confirmed },
{ ...createMockUser("5"), status: OrganizationUserStatusType.Confirmed },
{ ...createMockUser("6"), status: OrganizationUserStatusType.Confirmed },
{ ...createMockUser("7"), status: OrganizationUserStatusType.Revoked },
];
dataSource.data = users;
expect(dataSource.invitedUserCount).toBe(2);
expect(dataSource.acceptedUserCount).toBe(1);
expect(dataSource.confirmedUserCount).toBe(3);
expect(dataSource.revokedUserCount).toBe(1);
expect(dataSource.activeUserCount).toBe(6); // All except revoked
});
});
});

View File

@@ -1,14 +1,30 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { computed, Signal } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { map } from "rxjs";
import {
OrganizationUserStatusType,
ProviderUserStatusType,
} 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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { TableDataSource } from "@bitwarden/components";
import { StatusType, UserViewTypes } from "./base-members.component";
const MaxCheckedCount = 500;
/**
* Default maximum for most bulk operations (confirm, remove, delete, etc.)
*/
export const MaxCheckedCount = 500;
/**
* Maximum for bulk reinvite operations when the IncreaseBulkReinviteLimitForCloud
* feature flag is enabled on cloud environments.
*/
export const CloudBulkReinviteLimit = 4000;
/**
* Returns true if the user matches the status, or where the status is `null`, if the user is active (not revoked).
@@ -56,6 +72,20 @@ export abstract class PeopleTableDataSource<T extends UserViewTypes> extends Tab
confirmedUserCount: number;
revokedUserCount: number;
/** True when increased bulk limit feature is enabled (feature flag + cloud environment) */
readonly isIncreasedBulkLimitEnabled: Signal<boolean>;
constructor(configService: ConfigService, environmentService: EnvironmentService) {
super();
const featureFlagEnabled = toSignal(
configService.getFeatureFlag$(FeatureFlag.IncreaseBulkReinviteLimitForCloud),
);
const isCloud = toSignal(environmentService.environment$.pipe(map((env) => env.isCloud())));
this.isIncreasedBulkLimitEnabled = computed(() => featureFlagEnabled() && isCloud());
}
override set data(data: T[]) {
super.data = data;
@@ -89,6 +119,14 @@ export abstract class PeopleTableDataSource<T extends UserViewTypes> extends Tab
return this.data.filter((u) => (u as any).checked);
}
/**
* Gets checked users in the order they appear in the filtered/sorted table view.
* Use this when enforcing limits to ensure visual consistency (top N visible rows stay checked).
*/
getCheckedUsersInVisibleOrder() {
return this.filteredData.filter((u) => (u as any).checked);
}
/**
* Check all filtered users (i.e. those rows that are currently visible)
* @param select check the filtered users (true) or uncheck the filtered users (false)
@@ -101,8 +139,13 @@ export abstract class PeopleTableDataSource<T extends UserViewTypes> extends Tab
const filteredUsers = this.filteredData;
const selectCount =
filteredUsers.length > MaxCheckedCount ? MaxCheckedCount : filteredUsers.length;
// When the increased bulk limit feature is enabled, allow checking all users.
// Individual bulk operations will enforce their specific limits.
// When disabled, enforce the legacy limit at check time.
const selectCount = this.isIncreasedBulkLimitEnabled()
? filteredUsers.length
: Math.min(filteredUsers.length, MaxCheckedCount);
for (let i = 0; i < selectCount; i++) {
this.checkUser(filteredUsers[i], select);
}
@@ -132,4 +175,41 @@ export abstract class PeopleTableDataSource<T extends UserViewTypes> extends Tab
this.data = updatedData;
}
}
/**
* Limits an array of users and unchecks those beyond the limit.
* Returns the limited array.
*
* @param users The array of users to limit
* @param limit The maximum number of users to keep
* @returns The users array limited to the specified count
*/
limitAndUncheckExcess(users: T[], limit: number): T[] {
if (users.length <= limit) {
return users;
}
// Uncheck users beyond the limit
users.slice(limit).forEach((user) => this.checkUser(user, false));
return users.slice(0, limit);
}
/**
* Gets checked users with optional limiting based on the IncreaseBulkReinviteLimitForCloud feature flag.
*
* When the feature flag is enabled: Returns checked users in visible order, limited to the specified count.
* When the feature flag is disabled: Returns all checked users without applying any limit.
*
* @param limit The maximum number of users to return (only applied when feature flag is enabled)
* @returns The checked users array
*/
getCheckedUsersWithLimit(limit: number): T[] {
if (this.isIncreasedBulkLimitEnabled()) {
const allUsers = this.getCheckedUsersInVisibleOrder();
return this.limitAndUncheckExcess(allUsers, limit);
} else {
return this.getCheckedUsers();
}
}
}

View File

@@ -33,6 +33,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationMetadataServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-metadata.service.abstraction";
import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
@@ -44,7 +46,11 @@ import { BillingConstraintService } from "@bitwarden/web-vault/app/billing/membe
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
import { BaseMembersComponent } from "../../common/base-members.component";
import { PeopleTableDataSource } from "../../common/people-table-data-source";
import {
CloudBulkReinviteLimit,
MaxCheckedCount,
PeopleTableDataSource,
} from "../../common/people-table-data-source";
import { OrganizationUserView } from "../core/views/organization-user.view";
import { AccountRecoveryDialogResultType } from "./components/account-recovery/account-recovery-dialog.component";
@@ -70,7 +76,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
userType = OrganizationUserType;
userStatusType = OrganizationUserStatusType;
memberTab = MemberDialogTab;
protected dataSource = new MembersTableDataSource();
protected dataSource: MembersTableDataSource;
readonly organization: Signal<Organization | undefined>;
status: OrganizationUserStatusType | undefined;
@@ -113,6 +119,8 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
private policyService: PolicyService,
private policyApiService: PolicyApiServiceAbstraction,
private organizationMetadataService: OrganizationMetadataServiceAbstraction,
private configService: ConfigService,
private environmentService: EnvironmentService,
) {
super(
apiService,
@@ -126,6 +134,8 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
toastService,
);
this.dataSource = new MembersTableDataSource(this.configService, this.environmentService);
const organization$ = this.route.params.pipe(
concatMap((params) =>
this.userId$.pipe(
@@ -356,10 +366,9 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
return;
}
await this.memberDialogManager.openBulkRemoveDialog(
organization,
this.dataSource.getCheckedUsers(),
);
const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount);
await this.memberDialogManager.openBulkRemoveDialog(organization, users);
this.organizationMetadataService.refreshMetadataCache();
await this.load(organization);
}
@@ -369,10 +378,9 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
return;
}
await this.memberDialogManager.openBulkDeleteDialog(
organization,
this.dataSource.getCheckedUsers(),
);
const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount);
await this.memberDialogManager.openBulkDeleteDialog(organization, users);
await this.load(organization);
}
@@ -389,11 +397,9 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
return;
}
await this.memberDialogManager.openBulkRestoreRevokeDialog(
organization,
this.dataSource.getCheckedUsers(),
isRevoking,
);
const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount);
await this.memberDialogManager.openBulkRestoreRevokeDialog(organization, users, isRevoking);
await this.load(organization);
}
@@ -402,8 +408,28 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
return;
}
const users = this.dataSource.getCheckedUsers();
const filteredUsers = users.filter((u) => u.status === OrganizationUserStatusType.Invited);
let users: OrganizationUserView[];
if (this.dataSource.isIncreasedBulkLimitEnabled()) {
users = this.dataSource.getCheckedUsersInVisibleOrder();
} else {
users = this.dataSource.getCheckedUsers();
}
const allInvitedUsers = users.filter((u) => u.status === OrganizationUserStatusType.Invited);
// Capture the original count BEFORE enforcing the limit
const originalInvitedCount = allInvitedUsers.length;
// When feature flag is enabled, limit invited users and uncheck the excess
let filteredUsers: OrganizationUserView[];
if (this.dataSource.isIncreasedBulkLimitEnabled()) {
filteredUsers = this.dataSource.limitAndUncheckExcess(
allInvitedUsers,
CloudBulkReinviteLimit,
);
} else {
filteredUsers = allInvitedUsers;
}
if (filteredUsers.length <= 0) {
this.toastService.showToast({
@@ -424,13 +450,37 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
throw new Error();
}
// Bulk Status component open
await this.memberDialogManager.openBulkStatusDialog(
users,
filteredUsers,
Promise.resolve(result.successful),
this.i18nService.t("bulkReinviteMessage"),
);
// When feature flag is enabled, show toast instead of dialog
if (this.dataSource.isIncreasedBulkLimitEnabled()) {
const selectedCount = originalInvitedCount;
const invitedCount = filteredUsers.length;
if (selectedCount > CloudBulkReinviteLimit) {
const excludedCount = selectedCount - CloudBulkReinviteLimit;
this.toastService.showToast({
variant: "success",
message: this.i18nService.t(
"bulkReinviteLimitedSuccessToast",
CloudBulkReinviteLimit.toLocaleString(),
selectedCount.toLocaleString(),
excludedCount.toLocaleString(),
),
});
} else {
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("bulkReinviteSuccessToast", invitedCount.toString()),
});
}
} else {
// Feature flag disabled - show legacy dialog
await this.memberDialogManager.openBulkStatusDialog(
users,
filteredUsers,
Promise.resolve(result.successful),
this.i18nService.t("bulkReinviteMessage"),
);
}
} catch (e) {
this.validationService.showError(e);
}
@@ -442,15 +492,14 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
return;
}
await this.memberDialogManager.openBulkConfirmDialog(
organization,
this.dataSource.getCheckedUsers(),
);
const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount);
await this.memberDialogManager.openBulkConfirmDialog(organization, users);
await this.load(organization);
}
async bulkEnableSM(organization: Organization) {
const users = this.dataSource.getCheckedUsers();
const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount);
await this.memberDialogManager.openBulkEnableSecretsManagerDialog(organization, users);

View File

@@ -6457,6 +6457,32 @@
"bulkReinviteMessage": {
"message": "Reinvited successfully"
},
"bulkReinviteSuccessToast": {
"message": "$COUNT$ users re-invited",
"placeholders": {
"count": {
"content": "$1",
"example": "12"
}
}
},
"bulkReinviteLimitedSuccessToast": {
"message": "$LIMIT$ of $SELECTEDCOUNT$ users re-invited. $EXCLUDEDCOUNT$ were not invited due to the $LIMIT$ invite limit.",
"placeholders": {
"limit": {
"content": "$1",
"example": "4,000"
},
"selectedCount": {
"content": "$2",
"example": "4,005"
},
"excludedCount": {
"content": "$3",
"example": "5"
}
}
},
"bulkRemovedMessage": {
"message": "Removed successfully"
},

View File

@@ -19,6 +19,8 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { assertNonNullish } from "@bitwarden/common/auth/utils";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
@@ -27,6 +29,8 @@ import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { BaseMembersComponent } from "@bitwarden/web-vault/app/admin-console/common/base-members.component";
import {
CloudBulkReinviteLimit,
MaxCheckedCount,
peopleFilter,
PeopleTableDataSource,
} from "@bitwarden/web-vault/app/admin-console/common/people-table-data-source";
@@ -56,7 +60,7 @@ class MembersTableDataSource extends PeopleTableDataSource<ProviderUser> {
})
export class MembersComponent extends BaseMembersComponent<ProviderUser> {
accessEvents = false;
dataSource = new MembersTableDataSource();
dataSource: MembersTableDataSource;
loading = true;
providerId: string;
rowHeight = 70;
@@ -81,6 +85,8 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
private providerService: ProviderService,
private router: Router,
private accountService: AccountService,
private configService: ConfigService,
private environmentService: EnvironmentService,
) {
super(
apiService,
@@ -94,6 +100,8 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
toastService,
);
this.dataSource = new MembersTableDataSource(this.configService, this.environmentService);
combineLatest([
this.activatedRoute.parent.params,
this.activatedRoute.queryParams.pipe(first()),
@@ -134,10 +142,12 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
return;
}
const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount);
const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, {
data: {
providerId: this.providerId,
users: this.dataSource.getCheckedUsers(),
users: users,
},
});
@@ -150,10 +160,28 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
return;
}
const checkedUsers = this.dataSource.getCheckedUsers();
const checkedInvitedUsers = checkedUsers.filter(
(user) => user.status === ProviderUserStatusType.Invited,
);
let users: ProviderUser[];
if (this.dataSource.isIncreasedBulkLimitEnabled()) {
users = this.dataSource.getCheckedUsersInVisibleOrder();
} else {
users = this.dataSource.getCheckedUsers();
}
const allInvitedUsers = users.filter((user) => user.status === ProviderUserStatusType.Invited);
// Capture the original count BEFORE enforcing the limit
const originalInvitedCount = allInvitedUsers.length;
// When feature flag is enabled, limit invited users and uncheck the excess
let checkedInvitedUsers: ProviderUser[];
if (this.dataSource.isIncreasedBulkLimitEnabled()) {
checkedInvitedUsers = this.dataSource.limitAndUncheckExcess(
allInvitedUsers,
CloudBulkReinviteLimit,
);
} else {
checkedInvitedUsers = allInvitedUsers;
}
if (checkedInvitedUsers.length <= 0) {
this.toastService.showToast({
@@ -165,20 +193,50 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
}
try {
const request = this.apiService.postManyProviderUserReinvite(
this.providerId,
new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)),
);
// When feature flag is enabled, show toast instead of dialog
if (this.dataSource.isIncreasedBulkLimitEnabled()) {
await this.apiService.postManyProviderUserReinvite(
this.providerId,
new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)),
);
const dialogRef = BulkStatusComponent.open(this.dialogService, {
data: {
users: checkedUsers,
filteredUsers: checkedInvitedUsers,
request,
successfulMessage: this.i18nService.t("bulkReinviteMessage"),
},
});
await lastValueFrom(dialogRef.closed);
const selectedCount = originalInvitedCount;
const invitedCount = checkedInvitedUsers.length;
if (selectedCount > CloudBulkReinviteLimit) {
const excludedCount = selectedCount - CloudBulkReinviteLimit;
this.toastService.showToast({
variant: "success",
message: this.i18nService.t(
"bulkReinviteLimitedSuccessToast",
CloudBulkReinviteLimit.toLocaleString(),
selectedCount.toLocaleString(),
excludedCount.toLocaleString(),
),
});
} else {
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("bulkReinviteSuccessToast", invitedCount.toString()),
});
}
} else {
// Feature flag disabled - show legacy dialog
const request = this.apiService.postManyProviderUserReinvite(
this.providerId,
new ProviderUserBulkRequest(checkedInvitedUsers.map((user) => user.id)),
);
const dialogRef = BulkStatusComponent.open(this.dialogService, {
data: {
users: users,
filteredUsers: checkedInvitedUsers,
request,
successfulMessage: this.i18nService.t("bulkReinviteMessage"),
},
});
await lastValueFrom(dialogRef.closed);
}
} catch (error) {
this.validationService.showError(error);
}
@@ -193,10 +251,12 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
return;
}
const users = this.dataSource.getCheckedUsersWithLimit(MaxCheckedCount);
const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, {
data: {
providerId: this.providerId,
users: this.dataSource.getCheckedUsers(),
users: users,
},
});

View File

@@ -14,6 +14,7 @@ export enum FeatureFlag {
CreateDefaultLocation = "pm-19467-create-default-location",
AutoConfirm = "pm-19934-auto-confirm-organization-users",
BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration",
IncreaseBulkReinviteLimitForCloud = "pm-28251-increase-bulk-reinvite-limit-for-cloud",
/* Auth */
PM23801_PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin",
@@ -97,6 +98,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.CreateDefaultLocation]: FALSE,
[FeatureFlag.AutoConfirm]: FALSE,
[FeatureFlag.BlockClaimedDomainAccountCreation]: FALSE,
[FeatureFlag.IncreaseBulkReinviteLimitForCloud]: FALSE,
/* Autofill */
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,