mirror of
https://github.com/bitwarden/browser.git
synced 2025-03-02 03:41:09 +01:00
[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:
parent
e3f1150fcb
commit
d8689a20b5
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
@ -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" } };
|
||||
}
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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": {
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user