mirror of
https://github.com/bitwarden/browser.git
synced 2025-12-05 09:14:28 +01:00
Merge d4c90a221d into d32365fbba
This commit is contained in:
commit
8b75d34d3a
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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,23 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,14 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
return;
|
||||
}
|
||||
|
||||
await this.memberDialogManager.openBulkRemoveDialog(
|
||||
organization,
|
||||
this.dataSource.getCheckedUsers(),
|
||||
);
|
||||
const allUsers = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.getCheckedUsersInVisibleOrder()
|
||||
: this.dataSource.getCheckedUsers();
|
||||
const users = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.limitAndUncheckExcess(allUsers, MaxCheckedCount)
|
||||
: allUsers;
|
||||
|
||||
await this.memberDialogManager.openBulkRemoveDialog(organization, users);
|
||||
this.organizationMetadataService.refreshMetadataCache();
|
||||
await this.load(organization);
|
||||
}
|
||||
@ -369,10 +383,14 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
return;
|
||||
}
|
||||
|
||||
await this.memberDialogManager.openBulkDeleteDialog(
|
||||
organization,
|
||||
this.dataSource.getCheckedUsers(),
|
||||
);
|
||||
const allUsers = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.getCheckedUsersInVisibleOrder()
|
||||
: this.dataSource.getCheckedUsers();
|
||||
const users = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.limitAndUncheckExcess(allUsers, MaxCheckedCount)
|
||||
: allUsers;
|
||||
|
||||
await this.memberDialogManager.openBulkDeleteDialog(organization, users);
|
||||
await this.load(organization);
|
||||
}
|
||||
|
||||
@ -389,11 +407,14 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
return;
|
||||
}
|
||||
|
||||
await this.memberDialogManager.openBulkRestoreRevokeDialog(
|
||||
organization,
|
||||
this.dataSource.getCheckedUsers(),
|
||||
isRevoking,
|
||||
);
|
||||
const allUsers = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.getCheckedUsersInVisibleOrder()
|
||||
: this.dataSource.getCheckedUsers();
|
||||
const users = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.limitAndUncheckExcess(allUsers, MaxCheckedCount)
|
||||
: allUsers;
|
||||
|
||||
await this.memberDialogManager.openBulkRestoreRevokeDialog(organization, users, isRevoking);
|
||||
await this.load(organization);
|
||||
}
|
||||
|
||||
@ -402,8 +423,18 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
return;
|
||||
}
|
||||
|
||||
const users = this.dataSource.getCheckedUsers();
|
||||
const filteredUsers = users.filter((u) => u.status === OrganizationUserStatusType.Invited);
|
||||
const users = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.getCheckedUsersInVisibleOrder()
|
||||
: 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
|
||||
const filteredUsers = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.limitAndUncheckExcess(allInvitedUsers, CloudBulkReinviteLimit)
|
||||
: allInvitedUsers;
|
||||
|
||||
if (filteredUsers.length <= 0) {
|
||||
this.toastService.showToast({
|
||||
@ -424,13 +455,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 +497,24 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
return;
|
||||
}
|
||||
|
||||
await this.memberDialogManager.openBulkConfirmDialog(
|
||||
organization,
|
||||
this.dataSource.getCheckedUsers(),
|
||||
);
|
||||
const allUsers = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.getCheckedUsersInVisibleOrder()
|
||||
: this.dataSource.getCheckedUsers();
|
||||
const users = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.limitAndUncheckExcess(allUsers, MaxCheckedCount)
|
||||
: allUsers;
|
||||
|
||||
await this.memberDialogManager.openBulkConfirmDialog(organization, users);
|
||||
await this.load(organization);
|
||||
}
|
||||
|
||||
async bulkEnableSM(organization: Organization) {
|
||||
const users = this.dataSource.getCheckedUsers();
|
||||
const allUsers = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.getCheckedUsersInVisibleOrder()
|
||||
: this.dataSource.getCheckedUsers();
|
||||
const users = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.limitAndUncheckExcess(allUsers, MaxCheckedCount)
|
||||
: allUsers;
|
||||
|
||||
await this.memberDialogManager.openBulkEnableSecretsManagerDialog(organization, users);
|
||||
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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,17 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
return;
|
||||
}
|
||||
|
||||
const allUsers = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.getCheckedUsersInVisibleOrder()
|
||||
: this.dataSource.getCheckedUsers();
|
||||
const users = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.limitAndUncheckExcess(allUsers, MaxCheckedCount)
|
||||
: allUsers;
|
||||
|
||||
const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
providerId: this.providerId,
|
||||
users: this.dataSource.getCheckedUsers(),
|
||||
users: users,
|
||||
},
|
||||
});
|
||||
|
||||
@ -150,10 +165,18 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkedUsers = this.dataSource.getCheckedUsers();
|
||||
const checkedInvitedUsers = checkedUsers.filter(
|
||||
(user) => user.status === ProviderUserStatusType.Invited,
|
||||
);
|
||||
const users = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.getCheckedUsersInVisibleOrder()
|
||||
: 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
|
||||
const checkedInvitedUsers = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.limitAndUncheckExcess(allInvitedUsers, CloudBulkReinviteLimit)
|
||||
: allInvitedUsers;
|
||||
|
||||
if (checkedInvitedUsers.length <= 0) {
|
||||
this.toastService.showToast({
|
||||
@ -165,20 +188,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 +246,17 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
return;
|
||||
}
|
||||
|
||||
const allUsers = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.getCheckedUsersInVisibleOrder()
|
||||
: this.dataSource.getCheckedUsers();
|
||||
const users = this.dataSource.isIncreasedBulkLimitEnabled()
|
||||
? this.dataSource.limitAndUncheckExcess(allUsers, MaxCheckedCount)
|
||||
: allUsers;
|
||||
|
||||
const dialogRef = BulkRemoveDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
providerId: this.providerId,
|
||||
users: this.dataSource.getCheckedUsers(),
|
||||
users: users,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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",
|
||||
@ -96,6 +97,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.CreateDefaultLocation]: FALSE,
|
||||
[FeatureFlag.AutoConfirm]: FALSE,
|
||||
[FeatureFlag.BlockClaimedDomainAccountCreation]: FALSE,
|
||||
[FeatureFlag.IncreaseBulkReinviteLimitForCloud]: FALSE,
|
||||
|
||||
/* Autofill */
|
||||
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user