[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:
parent
4a552343f1
commit
2f44b9b0dd
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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
|
||||||
) {}
|
) {}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export { forbiddenCharacters } from "./forbidden-characters.validator";
|
export { forbiddenCharacters } from "./forbidden-characters.validator";
|
||||||
|
export { trimValidator } from "./trim.validator";
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue