[SM-628] Add trim validator to SM dialogs (#4993)

* Add trim validator to SM dialogs

* Swap to creating a generic component

* Swap to BitValidators.trimValidator

* Fix storybook

* update validator to auto trim whitespace

* update storybook copy

* fix copy

* update trim validator to run on submit

* add validator to project name in secret dialog; update secret name validation to on submit

---------

Co-authored-by: William Martin <contact@willmartian.com>
This commit is contained in:
Thomas Avery 2023-05-30 17:52:02 -05:00 committed by GitHub
parent 4a552343f1
commit 2f44b9b0dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 149 additions and 8 deletions

View File

@ -6776,6 +6776,10 @@
} }
} }
}, },
"inputTrimValidator": {
"message": "Input must not contain only whitespace.",
"description": "Notification to inform the user that a form's input can't contain only whitespace."
},
"dismiss": { "dismiss": {
"message": "Dismiss" "message": "Dismiss"
}, },

View File

@ -5,6 +5,7 @@ import { Router } from "@angular/router";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { BitValidators } from "@bitwarden/components";
import { ProjectView } from "../../models/view/project.view"; import { ProjectView } from "../../models/view/project.view";
import { ProjectService } from "../../projects/project.service"; import { ProjectService } from "../../projects/project.service";
@ -25,7 +26,10 @@ export interface ProjectOperation {
}) })
export class ProjectDialogComponent implements OnInit { export class ProjectDialogComponent implements OnInit {
protected formGroup = new FormGroup({ protected formGroup = new FormGroup({
name: new FormControl("", [Validators.required]), name: new FormControl("", {
validators: [Validators.required, BitValidators.trimValidator],
updateOn: "submit",
}),
}); });
protected loading = false; protected loading = false;

View File

@ -8,6 +8,7 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Utils } from "@bitwarden/common/misc/utils"; import { Utils } from "@bitwarden/common/misc/utils";
import { BitValidators } from "@bitwarden/components";
import { ProjectListView } from "../../models/view/project-list.view"; import { ProjectListView } from "../../models/view/project-list.view";
import { ProjectView } from "../../models/view/project.view"; import { ProjectView } from "../../models/view/project.view";
@ -36,11 +37,20 @@ export interface SecretOperation {
}) })
export class SecretDialogComponent implements OnInit { export class SecretDialogComponent implements OnInit {
protected formGroup = new FormGroup({ protected formGroup = new FormGroup({
name: new FormControl("", [Validators.required]), name: new FormControl("", {
validators: [Validators.required, BitValidators.trimValidator],
updateOn: "submit",
}),
value: new FormControl("", [Validators.required]), value: new FormControl("", [Validators.required]),
notes: new FormControl(""), notes: new FormControl("", {
validators: [BitValidators.trimValidator],
updateOn: "submit",
}),
project: new FormControl("", [Validators.required]), project: new FormControl("", [Validators.required]),
newProjectName: new FormControl(""), newProjectName: new FormControl("", {
validators: [BitValidators.trimValidator],
updateOn: "submit",
}),
}); });
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();

View File

@ -3,6 +3,8 @@ import { Component, Inject, OnInit } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms"; import { FormControl, FormGroup, Validators } from "@angular/forms";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { BitValidators } from "@bitwarden/components";
import { ServiceAccountView } from "../../../models/view/service-account.view"; import { ServiceAccountView } from "../../../models/view/service-account.view";
import { AccessTokenView } from "../../models/view/access-token.view"; import { AccessTokenView } from "../../models/view/access-token.view";
@ -20,7 +22,10 @@ export interface AccessTokenOperation {
}) })
export class AccessTokenCreateDialogComponent implements OnInit { export class AccessTokenCreateDialogComponent implements OnInit {
protected formGroup = new FormGroup({ protected formGroup = new FormGroup({
name: new FormControl("", [Validators.required, Validators.maxLength(80)]), name: new FormControl("", {
validators: [Validators.required, Validators.maxLength(80), BitValidators.trimValidator],
updateOn: "submit",
}),
expirationDateControl: new FormControl(null), expirationDateControl: new FormControl(null),
}); });
protected loading = false; protected loading = false;
@ -30,6 +35,7 @@ export class AccessTokenCreateDialogComponent implements OnInit {
constructor( constructor(
public dialogRef: DialogRef, public dialogRef: DialogRef,
@Inject(DIALOG_DATA) public data: AccessTokenOperation, @Inject(DIALOG_DATA) public data: AccessTokenOperation,
private i18nService: I18nService,
private dialogService: DialogServiceAbstraction, private dialogService: DialogServiceAbstraction,
private accessService: AccessService private accessService: AccessService
) {} ) {}

View File

@ -4,6 +4,7 @@ import { FormControl, FormGroup, Validators } from "@angular/forms";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { BitValidators } from "@bitwarden/components";
import { ServiceAccountView } from "../../models/view/service-account.view"; import { ServiceAccountView } from "../../models/view/service-account.view";
import { ServiceAccountService } from "../service-account.service"; import { ServiceAccountService } from "../service-account.service";
@ -23,9 +24,15 @@ export interface ServiceAccountOperation {
templateUrl: "./service-account-dialog.component.html", templateUrl: "./service-account-dialog.component.html",
}) })
export class ServiceAccountDialogComponent { export class ServiceAccountDialogComponent {
protected formGroup = new FormGroup({ protected formGroup = new FormGroup(
name: new FormControl("", [Validators.required]), {
}); name: new FormControl("", {
validators: [Validators.required, BitValidators.trimValidator],
updateOn: "submit",
}),
},
{}
);
protected loading = false; protected loading = false;

View File

@ -8,6 +8,7 @@ import { InputModule } from "../input/input.module";
import { I18nMockService } from "../utils/i18n-mock.service"; import { I18nMockService } from "../utils/i18n-mock.service";
import { forbiddenCharacters } from "./bit-validators/forbidden-characters.validator"; import { forbiddenCharacters } from "./bit-validators/forbidden-characters.validator";
import { trimValidator } from "./bit-validators/trim.validator";
import { BitFormFieldComponent } from "./form-field.component"; import { BitFormFieldComponent } from "./form-field.component";
import { FormFieldModule } from "./form-field.module"; import { FormFieldModule } from "./form-field.module";
@ -24,6 +25,7 @@ export default {
return new I18nMockService({ return new I18nMockService({
inputForbiddenCharacters: (chars) => inputForbiddenCharacters: (chars) =>
`The following characters are not allowed: ${chars}`, `The following characters are not allowed: ${chars}`,
inputTrimValidator: "Input must not contain only whitespace.",
}); });
}, },
}, },
@ -56,3 +58,20 @@ export const ForbiddenCharacters: StoryObj<BitFormFieldComponent> = {
template, template,
}), }),
}; };
export const TrimValidator: StoryObj<BitFormFieldComponent> = {
render: (args: BitFormFieldComponent) => ({
props: {
formObj: new FormBuilder().group({
name: [
"",
{
updateOn: "submit",
validators: [trimValidator],
},
],
}),
},
template,
}),
};

View File

@ -1 +1,2 @@
export { forbiddenCharacters } from "./forbidden-characters.validator"; export { forbiddenCharacters } from "./forbidden-characters.validator";
export { trimValidator } from "./trim.validator";

View File

@ -0,0 +1,61 @@
import { FormControl } from "@angular/forms";
import { trimValidator as validate } from "./trim.validator";
describe("trimValidator", () => {
it("should not error when input is null", () => {
const input = createControl(null);
const errors = validate(input);
expect(errors).toBe(null);
});
it("should not error when input is an empty string", () => {
const input = createControl("");
const errors = validate(input);
expect(errors).toBe(null);
});
it("should not error when input has no whitespace", () => {
const input = createControl("test value");
const errors = validate(input);
expect(errors).toBe(null);
});
it("should remove beginning whitespace", () => {
const input = createControl(" test value");
const errors = validate(input);
expect(errors).toBe(null);
expect(input.value).toBe("test value");
});
it("should remove trailing whitespace", () => {
const input = createControl("test value ");
const errors = validate(input);
expect(errors).toBe(null);
expect(input.value).toBe("test value");
});
it("should remove beginning and trailing whitespace", () => {
const input = createControl(" test value ");
const errors = validate(input);
expect(errors).toBe(null);
expect(input.value).toBe("test value");
});
it("should error when input is just whitespace", () => {
const input = createControl(" ");
const errors = validate(input);
expect(errors).toEqual({ trim: { message: "input is only whitespace" } });
});
});
function createControl(input: string) {
return new FormControl(input);
}

View File

@ -0,0 +1,27 @@
import { AbstractControl, FormControl, ValidatorFn } from "@angular/forms";
/**
* Automatically trims FormControl value. Errors if value only contains whitespace.
*
* Should be used with `updateOn: "submit"`
*/
export const trimValidator: ValidatorFn = (control: AbstractControl<string>) => {
if (!(control instanceof FormControl)) {
throw new Error("trimValidator only supports validating FormControls");
}
const value = control.value;
if (value === null || value === undefined || value === "") {
return null;
}
if (!value.trim().length) {
return {
trim: {
message: "input is only whitespace",
},
};
}
if (value !== value.trim()) {
control.setValue(value.trim());
}
return null;
};

View File

@ -38,6 +38,8 @@ export class BitErrorComponent {
return this.i18nService.t("inputForbiddenCharacters", this.error[1]?.characters.join(", ")); return this.i18nService.t("inputForbiddenCharacters", this.error[1]?.characters.join(", "));
case "multipleEmails": case "multipleEmails":
return this.i18nService.t("multipleInputEmails"); return this.i18nService.t("multipleInputEmails");
case "trim":
return this.i18nService.t("inputTrimValidator");
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) {