[EC-1016][EC-1017][EC-1018] add validation to collection dialog (#4528)

* [EC-1016] add validation to collection dialog

* [EC-1017] add validation to members dialog

* [EC-1017] remove unused imports from members tab

* [EC-1017] move validator out of shared module

* [EC-1018] add validation to group modal
This commit is contained in:
Jake Fink 2023-01-25 11:03:09 -05:00 committed by GitHub
parent e3f1150fcb
commit d8689a20b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 128 additions and 11 deletions

View File

@ -16,7 +16,7 @@
<span class="sr-only">{{ "loading" | i18n }}</span>
</div>
<bit-tab-group *ngIf="!loading" [selectedIndex]="tabIndex">
<bit-tab-group *ngIf="!loading" [(selectedIndex)]="tabIndex">
<bit-tab label="{{ 'groupInfo' | i18n }}">
<bit-form-field>
<bit-label>{{ "name" | i18n }}</bit-label>

View File

@ -227,7 +227,16 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
}
submit = async () => {
this.groupForm.markAllAsTouched();
if (this.groupForm.invalid) {
if (this.tabIndex !== GroupAddEditTabType.Info) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("groupInfo"))
);
}
return;
}

View File

@ -16,7 +16,7 @@
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<bit-tab-group *ngIf="!loading" [selectedIndex]="tabIndex">
<bit-tab-group *ngIf="!loading" [(selectedIndex)]="tabIndex">
<bit-tab [label]="'role' | i18n">
<ng-container *ngIf="!editMode">
<p>{{ "inviteUserDesc" | i18n }}</p>

View File

@ -3,8 +3,6 @@ import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { combineLatest, of, shareReplay, Subject, switchMap, takeUntil } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
@ -33,6 +31,8 @@ import {
PermissionMode,
} from "../../../shared/components/access-selector";
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
export enum MemberDialogTab {
Role = 0,
Groups = 1,
@ -75,7 +75,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
protected groupAccessItems: AccessItemView[] = [];
protected tabIndex: MemberDialogTab;
protected formGroup = this.formBuilder.group({
emails: ["", [Validators.required]],
emails: ["", [Validators.required, commaSeparatedEmails]],
type: OrganizationUserType.User,
accessAllCollections: false,
access: [[] as AccessItemValue[]],
@ -117,9 +117,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
constructor(
@Inject(DIALOG_DATA) protected params: MemberDialogParams,
private dialogRef: DialogRef<MemberDialogResult>,
private apiService: ApiService,
private i18nService: I18nService,
private collectionService: CollectionService,
private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService,
private formBuilder: FormBuilder,
@ -291,7 +289,16 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
}
submit = async () => {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
if (this.tabIndex !== MemberDialogTab.Role) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("role"))
);
}
return;
}
@ -323,6 +330,12 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
} else {
userView.id = this.params.organizationUserId;
const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))];
if (emails.length > 20) {
this.formGroup.controls.emails.setErrors({
tooManyEmails: { message: this.i18nService.t("tooManyEmails", 20) },
});
return;
}
await this.userService.invite(emails, userView);
}

View File

@ -0,0 +1,46 @@
import { FormControl } from "@angular/forms";
import { commaSeparatedEmails } from "./comma-separated-emails.validator";
describe("commaSeparatedEmails", () => {
it("should return no error when input is valid", () => {
const input = createControl(null);
input.setValue("user@bitwarden.com");
const errors = commaSeparatedEmails(input);
expect(errors).toBe(null);
});
it("should return no error when a single valid email is provided", () => {
const input = createControl("user@bitwarden.com");
const errors = commaSeparatedEmails(input);
expect(errors).toBe(null);
});
it("should return no error when input has valid emails separated by commas", () => {
const input = createControl("user@bitwarden.com, user1@bitwarden.com, user@bitwarden.com");
const errors = commaSeparatedEmails(input);
expect(errors).toBe(null);
});
it("should return error when input is invalid", () => {
const input = createControl("lksjflks");
const errors = commaSeparatedEmails(input);
expect(errors).not.toBe(null);
});
it("should return error when input contains invalid emails", () => {
const input = createControl("user@bitwarden.com, nonsfonwoei, user1@bitwarden.com");
const errors = commaSeparatedEmails(input);
expect(errors).not.toBe(null);
});
});
function createControl(input: string) {
return new FormControl(input);
}

View File

@ -0,0 +1,17 @@
import { AbstractControl, ValidationErrors, Validators } from "@angular/forms";
function validateEmails(emails: string) {
return (
emails
.split(",")
.map((email) => Validators.email(<AbstractControl>{ value: email.trim() }))
.find((_) => _ !== null) === undefined
);
}
export function commaSeparatedEmails(control: AbstractControl): ValidationErrors | null {
if (control.value === "" || !control.value || validateEmails(control.value)) {
return null;
}
return { multipleEmails: { message: "multipleInputEmails" } };
}

View File

@ -15,11 +15,11 @@
<ng-container *ngIf="loading" #spinner>
<i class="bwi bwi-spinner bwi-lg bwi-spin" aria-hidden="true"></i>
</ng-container>
<bit-tab-group *ngIf="!loading" [selectedIndex]="tabIndex">
<bit-tab-group *ngIf="!loading" [(selectedIndex)]="tabIndex">
<bit-tab label="{{ 'collectionInfo' | i18n }}">
<bit-form-field>
<bit-label>{{ "name" | i18n }}</bit-label>
<input bitInput formControlName="name" required />
<input bitInput formControlName="name" />
</bit-form-field>
<bit-form-field>

View File

@ -1,6 +1,6 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { FormBuilder, Validators } from "@angular/forms";
import { combineLatest, of, shareReplay, Subject, switchMap, takeUntil } from "rxjs";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
@ -60,7 +60,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
protected accessItems: AccessItemView[] = [];
protected deletedParentName: string | undefined;
protected formGroup = this.formBuilder.group({
name: ["", BitValidators.forbiddenCharacters(["/"])],
name: ["", [Validators.required, BitValidators.forbiddenCharacters(["/"])]],
externalId: "",
parent: null as string | null,
access: [[] as AccessItemValue[]],
@ -155,7 +155,16 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
}
protected submit = async () => {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
if (this.tabIndex === CollectionDialogTabType.Access) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("collectionInfo"))
);
}
return;
}

View File

@ -2494,6 +2494,15 @@
"editMember": {
"message": "Edit member"
},
"fieldOnTabRequiresAttention": {
"message": "A field on the '$TAB$' tab requires your attention.",
"placeholders": {
"tab": {
"content": "$1",
"example": "Collection info"
}
}
},
"inviteUserDesc": {
"message": "Invite a new user to your organization by entering their Bitwarden account email address below. If they do not have a Bitwarden account already, they will be prompted to create a new account."
},
@ -5553,6 +5562,18 @@
}
}
},
"multipleInputEmails": {
"message": "1 or more emails are invalid"
},
"tooManyEmails": {
"message": "You can only submit up to $COUNT$ emails at a time",
"placeholders": {
"count": {
"content": "$1",
"example": "20"
}
}
},
"fieldsNeedAttention": {
"message": "$COUNT$ field(s) above need your attention.",
"placeholders": {

View File

@ -32,6 +32,8 @@ export class BitErrorComponent {
return this.i18nService.t("inputMaxLength", this.error[1]?.requiredLength);
case "forbiddenCharacters":
return this.i18nService.t("inputForbiddenCharacters", this.error[1]?.characters.join(", "));
case "multipleEmails":
return this.i18nService.t("multipleInputEmails");
default:
// Attempt to show a custom error message.
if (this.error[1]?.message) {