[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>
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<bit-tab-group *ngIf="!loading" [selectedIndex]="tabIndex">
|
<bit-tab-group *ngIf="!loading" [(selectedIndex)]="tabIndex">
|
||||||
<bit-tab label="{{ 'groupInfo' | i18n }}">
|
<bit-tab label="{{ 'groupInfo' | i18n }}">
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "name" | i18n }}</bit-label>
|
<bit-label>{{ "name" | i18n }}</bit-label>
|
||||||
|
|
|
@ -227,7 +227,16 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
|
this.groupForm.markAllAsTouched();
|
||||||
|
|
||||||
if (this.groupForm.invalid) {
|
if (this.groupForm.invalid) {
|
||||||
|
if (this.tabIndex !== GroupAddEditTabType.Info) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("groupInfo"))
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
></i>
|
></i>
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<bit-tab-group *ngIf="!loading" [selectedIndex]="tabIndex">
|
<bit-tab-group *ngIf="!loading" [(selectedIndex)]="tabIndex">
|
||||||
<bit-tab [label]="'role' | i18n">
|
<bit-tab [label]="'role' | i18n">
|
||||||
<ng-container *ngIf="!editMode">
|
<ng-container *ngIf="!editMode">
|
||||||
<p>{{ "inviteUserDesc" | i18n }}</p>
|
<p>{{ "inviteUserDesc" | i18n }}</p>
|
||||||
|
|
|
@ -3,8 +3,6 @@ import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { FormBuilder, Validators } from "@angular/forms";
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
import { combineLatest, of, shareReplay, Subject, switchMap, takeUntil } from "rxjs";
|
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 { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||||
|
@ -33,6 +31,8 @@ import {
|
||||||
PermissionMode,
|
PermissionMode,
|
||||||
} from "../../../shared/components/access-selector";
|
} from "../../../shared/components/access-selector";
|
||||||
|
|
||||||
|
import { commaSeparatedEmails } from "./validators/comma-separated-emails.validator";
|
||||||
|
|
||||||
export enum MemberDialogTab {
|
export enum MemberDialogTab {
|
||||||
Role = 0,
|
Role = 0,
|
||||||
Groups = 1,
|
Groups = 1,
|
||||||
|
@ -75,7 +75,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]],
|
emails: ["", [Validators.required, commaSeparatedEmails]],
|
||||||
type: OrganizationUserType.User,
|
type: OrganizationUserType.User,
|
||||||
accessAllCollections: false,
|
accessAllCollections: false,
|
||||||
access: [[] as AccessItemValue[]],
|
access: [[] as AccessItemValue[]],
|
||||||
|
@ -117,9 +117,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DIALOG_DATA) protected params: MemberDialogParams,
|
@Inject(DIALOG_DATA) protected params: MemberDialogParams,
|
||||||
private dialogRef: DialogRef<MemberDialogResult>,
|
private dialogRef: DialogRef<MemberDialogResult>,
|
||||||
private apiService: ApiService,
|
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private collectionService: CollectionService,
|
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
|
@ -291,7 +289,16 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
|
this.formGroup.markAllAsTouched();
|
||||||
|
|
||||||
if (this.formGroup.invalid) {
|
if (this.formGroup.invalid) {
|
||||||
|
if (this.tabIndex !== MemberDialogTab.Role) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("role"))
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,6 +330,12 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||||
} else {
|
} else {
|
||||||
userView.id = this.params.organizationUserId;
|
userView.id = this.params.organizationUserId;
|
||||||
const emails = [...new Set(this.formGroup.value.emails.trim().split(/\s*,\s*/))];
|
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);
|
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>
|
<ng-container *ngIf="loading" #spinner>
|
||||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" aria-hidden="true"></i>
|
<i class="bwi bwi-spinner bwi-lg bwi-spin" aria-hidden="true"></i>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<bit-tab-group *ngIf="!loading" [selectedIndex]="tabIndex">
|
<bit-tab-group *ngIf="!loading" [(selectedIndex)]="tabIndex">
|
||||||
<bit-tab label="{{ 'collectionInfo' | i18n }}">
|
<bit-tab label="{{ 'collectionInfo' | i18n }}">
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "name" | i18n }}</bit-label>
|
<bit-label>{{ "name" | i18n }}</bit-label>
|
||||||
<input bitInput formControlName="name" required />
|
<input bitInput formControlName="name" />
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
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 { combineLatest, of, shareReplay, Subject, switchMap, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
@ -60,7 +60,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||||
protected accessItems: AccessItemView[] = [];
|
protected accessItems: AccessItemView[] = [];
|
||||||
protected deletedParentName: string | undefined;
|
protected deletedParentName: string | undefined;
|
||||||
protected formGroup = this.formBuilder.group({
|
protected formGroup = this.formBuilder.group({
|
||||||
name: ["", BitValidators.forbiddenCharacters(["/"])],
|
name: ["", [Validators.required, BitValidators.forbiddenCharacters(["/"])]],
|
||||||
externalId: "",
|
externalId: "",
|
||||||
parent: null as string | null,
|
parent: null as string | null,
|
||||||
access: [[] as AccessItemValue[]],
|
access: [[] as AccessItemValue[]],
|
||||||
|
@ -155,7 +155,16 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected submit = async () => {
|
protected submit = async () => {
|
||||||
|
this.formGroup.markAllAsTouched();
|
||||||
|
|
||||||
if (this.formGroup.invalid) {
|
if (this.formGroup.invalid) {
|
||||||
|
if (this.tabIndex === CollectionDialogTabType.Access) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("collectionInfo"))
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2494,6 +2494,15 @@
|
||||||
"editMember": {
|
"editMember": {
|
||||||
"message": "Edit member"
|
"message": "Edit member"
|
||||||
},
|
},
|
||||||
|
"fieldOnTabRequiresAttention": {
|
||||||
|
"message": "A field on the '$TAB$' tab requires your attention.",
|
||||||
|
"placeholders": {
|
||||||
|
"tab": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Collection info"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"inviteUserDesc": {
|
"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."
|
"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": {
|
"fieldsNeedAttention": {
|
||||||
"message": "$COUNT$ field(s) above need your attention.",
|
"message": "$COUNT$ field(s) above need your attention.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
|
|
@ -32,6 +32,8 @@ export class BitErrorComponent {
|
||||||
return this.i18nService.t("inputMaxLength", this.error[1]?.requiredLength);
|
return this.i18nService.t("inputMaxLength", this.error[1]?.requiredLength);
|
||||||
case "forbiddenCharacters":
|
case "forbiddenCharacters":
|
||||||
return this.i18nService.t("inputForbiddenCharacters", this.error[1]?.characters.join(", "));
|
return this.i18nService.t("inputForbiddenCharacters", this.error[1]?.characters.join(", "));
|
||||||
|
case "multipleEmails":
|
||||||
|
return this.i18nService.t("multipleInputEmails");
|
||||||
default:
|
default:
|
||||||
// Attempt to show a custom error message.
|
// Attempt to show a custom error message.
|
||||||
if (this.error[1]?.message) {
|
if (this.error[1]?.message) {
|
||||||
|
|
Loading…
Reference in New Issue