[PM-146] Web: Upgrade flows for free 2 person orgs (#5564)
* Added a validator when adding users to a free org * Updated based on PR feedback Removed parameters passing in the org to member-dialog. Removed i18n service from validator * Moved i18n responsibility back to the validator Also added jsdoc comments * Updated validator to be an injectable class * Added back in jsdocs * Moved the validator initialization to ngOnInit * Updated validator to take error message a a param
This commit is contained in:
parent
7dbc30ee05
commit
d4f292108f
|
@ -35,6 +35,7 @@ import {
|
||||||
} from "../../../shared/components/access-selector";
|
} from "../../../shared/components/access-selector";
|
||||||
|
|
||||||
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
|
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
|
||||||
|
import { freeOrgSeatLimitReachedValidator } from "./validators/free-org-inv-limit-reached.validator";
|
||||||
|
|
||||||
export enum MemberDialogTab {
|
export enum MemberDialogTab {
|
||||||
Role = 0,
|
Role = 0,
|
||||||
|
@ -46,6 +47,7 @@ export interface MemberDialogParams {
|
||||||
name: string;
|
name: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
organizationUserId: string;
|
organizationUserId: string;
|
||||||
|
allOrganizationUserEmails: string[];
|
||||||
usesKeyConnector: boolean;
|
usesKeyConnector: boolean;
|
||||||
initialTab?: MemberDialogTab;
|
initialTab?: MemberDialogTab;
|
||||||
}
|
}
|
||||||
|
@ -79,7 +81,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||||
protected groupAccessItems: AccessItemView[] = [];
|
protected groupAccessItems: AccessItemView[] = [];
|
||||||
protected tabIndex: MemberDialogTab;
|
protected tabIndex: MemberDialogTab;
|
||||||
protected formGroup = this.formBuilder.group({
|
protected formGroup = this.formBuilder.group({
|
||||||
emails: ["", [Validators.required, commaSeparatedEmails]],
|
emails: ["", { updateOn: "blur" }],
|
||||||
type: OrganizationUserType.User,
|
type: OrganizationUserType.User,
|
||||||
externalId: this.formBuilder.control({ value: "", disabled: true }),
|
externalId: this.formBuilder.control({ value: "", disabled: true }),
|
||||||
accessAllCollections: false,
|
accessAllCollections: false,
|
||||||
|
@ -167,6 +169,20 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||||
this.canUseCustomPermissions = organization.useCustomPermissions;
|
this.canUseCustomPermissions = organization.useCustomPermissions;
|
||||||
this.canUseSecretsManager = organization.useSecretsManager && flagEnabled("secretsManager");
|
this.canUseSecretsManager = organization.useSecretsManager && flagEnabled("secretsManager");
|
||||||
|
|
||||||
|
const emailsControlValidators = [
|
||||||
|
Validators.required,
|
||||||
|
commaSeparatedEmails,
|
||||||
|
freeOrgSeatLimitReachedValidator(
|
||||||
|
this.organization,
|
||||||
|
this.params.allOrganizationUserEmails,
|
||||||
|
this.i18nService.t("subscriptionFreePlan", organization.seats)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const emailsControl = this.formGroup.get("emails");
|
||||||
|
emailsControl.setValidators(emailsControlValidators);
|
||||||
|
emailsControl.updateValueAndValidity();
|
||||||
|
|
||||||
this.collectionAccessItems = [].concat(
|
this.collectionAccessItems = [].concat(
|
||||||
collections.map((c) => mapCollectionToAccessItemView(c))
|
collections.map((c) => mapCollectionToAccessItemView(c))
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { AbstractControl, FormControl, ValidationErrors } from "@angular/forms";
|
||||||
|
|
||||||
|
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { ProductType } from "@bitwarden/common/enums";
|
||||||
|
|
||||||
|
import { freeOrgSeatLimitReachedValidator } from "./free-org-inv-limit-reached.validator";
|
||||||
|
|
||||||
|
const orgFactory = (props: Partial<Organization> = {}) =>
|
||||||
|
Object.assign(
|
||||||
|
new Organization(),
|
||||||
|
{
|
||||||
|
id: "myOrgId",
|
||||||
|
enabled: true,
|
||||||
|
type: OrganizationUserType.Admin,
|
||||||
|
},
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("freeOrgSeatLimitReachedValidator", () => {
|
||||||
|
let organization: Organization;
|
||||||
|
let allOrganizationUserEmails: string[];
|
||||||
|
let validatorFn: (control: AbstractControl) => ValidationErrors | null;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
allOrganizationUserEmails = ["user1@example.com"];
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when control value is empty", () => {
|
||||||
|
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||||
|
organization,
|
||||||
|
allOrganizationUserEmails,
|
||||||
|
"You cannot invite more than 2 members without upgrading your plan."
|
||||||
|
);
|
||||||
|
const control = new FormControl("");
|
||||||
|
|
||||||
|
const result = validatorFn(control);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when control value is null", () => {
|
||||||
|
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||||
|
organization,
|
||||||
|
allOrganizationUserEmails,
|
||||||
|
"You cannot invite more than 2 members without upgrading your plan."
|
||||||
|
);
|
||||||
|
const control = new FormControl(null);
|
||||||
|
|
||||||
|
const result = validatorFn(control);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when max seats are not exceeded on free plan", () => {
|
||||||
|
organization = orgFactory({
|
||||||
|
planProductType: ProductType.Free,
|
||||||
|
seats: 2,
|
||||||
|
});
|
||||||
|
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||||
|
organization,
|
||||||
|
allOrganizationUserEmails,
|
||||||
|
"You cannot invite more than 2 members without upgrading your plan."
|
||||||
|
);
|
||||||
|
const control = new FormControl("user2@example.com");
|
||||||
|
|
||||||
|
const result = validatorFn(control);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return validation error when max seats are exceeded on free plan", () => {
|
||||||
|
organization = orgFactory({
|
||||||
|
planProductType: ProductType.Free,
|
||||||
|
seats: 2,
|
||||||
|
});
|
||||||
|
const errorMessage = "You cannot invite more than 2 members without upgrading your plan.";
|
||||||
|
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||||
|
organization,
|
||||||
|
allOrganizationUserEmails,
|
||||||
|
"You cannot invite more than 2 members without upgrading your plan."
|
||||||
|
);
|
||||||
|
const control = new FormControl("user2@example.com,user3@example.com");
|
||||||
|
|
||||||
|
const result = validatorFn(control);
|
||||||
|
|
||||||
|
expect(result).toStrictEqual({ freePlanLimitReached: { message: errorMessage } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when not on free plan", () => {
|
||||||
|
const control = new FormControl("user2@example.com,user3@example.com");
|
||||||
|
organization = orgFactory({
|
||||||
|
planProductType: ProductType.Enterprise,
|
||||||
|
seats: 100,
|
||||||
|
});
|
||||||
|
validatorFn = freeOrgSeatLimitReachedValidator(
|
||||||
|
organization,
|
||||||
|
allOrganizationUserEmails,
|
||||||
|
"You cannot invite more than 2 members without upgrading your plan."
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = validatorFn(control);
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
|
||||||
|
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { ProductType } from "@bitwarden/common/enums";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the limit of free organization seats has been reached when adding new users
|
||||||
|
* @param organization An object representing the organization
|
||||||
|
* @param allOrganizationUserEmails An array of strings with existing user email addresses
|
||||||
|
* @param errorMessage A localized string to display if validation fails
|
||||||
|
* @returns A function that validates an `AbstractControl` and returns `ValidationErrors` or `null`
|
||||||
|
*/
|
||||||
|
export function freeOrgSeatLimitReachedValidator(
|
||||||
|
organization: Organization,
|
||||||
|
allOrganizationUserEmails: string[],
|
||||||
|
errorMessage: string
|
||||||
|
): ValidatorFn {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
if (control.value === "" || !control.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEmailsToAdd = control.value
|
||||||
|
.split(",")
|
||||||
|
.filter(
|
||||||
|
(newEmailToAdd: string) =>
|
||||||
|
newEmailToAdd &&
|
||||||
|
!allOrganizationUserEmails.some((existingEmail) => existingEmail === newEmailToAdd)
|
||||||
|
);
|
||||||
|
|
||||||
|
return organization.planProductType === ProductType.Free &&
|
||||||
|
allOrganizationUserEmails.length + newEmailsToAdd.length > organization.seats
|
||||||
|
? { freePlanLimitReached: { message: errorMessage } }
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
}
|
|
@ -396,6 +396,7 @@ export class PeopleComponent
|
||||||
name: this.userNamePipe.transform(user),
|
name: this.userNamePipe.transform(user),
|
||||||
organizationId: this.organization.id,
|
organizationId: this.organization.id,
|
||||||
organizationUserId: user != null ? user.id : null,
|
organizationUserId: user != null ? user.id : null,
|
||||||
|
allOrganizationUserEmails: this.allUsers?.map((user) => user.email) ?? [],
|
||||||
usesKeyConnector: user?.usesKeyConnector,
|
usesKeyConnector: user?.usesKeyConnector,
|
||||||
initialTab: initialTab,
|
initialTab: initialTab,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue