feat: exclude emails and domains (#17591)

* feat: add constants

* feat: add excludeEmails field in baseSchema

* feat: add ExcludeEmails Input field in FieldEditDialog

* feat: add excludedEmails check in FormBuilderField

* chore: move validations in getBookingResponsesSchema

* Update i18n string to be more generic

* Update apps/web/public/static/locales/en/common.json

* feat: add validation on excludeEmails field

* feat: add fieldSchema as resolver

* chore: remove comment

* chore: format comments

* feat: add tests for excluded email or domains

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
This commit is contained in:
Vishnu
2024-11-15 10:46:25 +00:00
committed by GitHub
co-authored by Alex van Andel Peer Richelsen Anik Dhabal Babu
parent e0713d78b3
commit c59e733b5a
5 changed files with 137 additions and 8 deletions
@@ -2770,5 +2770,7 @@
"salesforce_route_to_custom_lookup_field": "Route to a user that matches a lookup field on an account",
"salesforce_option": "Salesforce Option",
"lookup_field_name": "Lookup Field Name",
"exclude_emails_that_contain" : "Exclude emails that contain ...",
"exclude_emails_match_found_error_message" : "Please enter a valid work email address",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -11,6 +11,7 @@ const CUSTOM_REQUIRED_FIELD_ERROR_MSG = "error_required_field";
const CUSTOM_PHONE_VALIDATION_ERROR_MSG = "invalid_number";
const CUSTOM_EMAIL_VALIDATION_ERROR_MSG = "email_validation_error";
const CUSTOM_URL_VALIDATION_ERROR_MSG = "url_validation_error";
const CUSTOM_EMAIL_EXCLUDED_ERROR_MSG = "exclude_emails_match_found_error_message";
const ZOD_REQUIRED_FIELD_ERROR_MSG = "Required";
describe("getBookingResponsesSchema", () => {
@@ -1241,3 +1242,79 @@ describe("getBookingResponsesPartialSchema - Prefill validation", () => {
);
});
});
describe("excluded email/domain validation", () => {
test("should fail if the email is present in excluded emails", async () => {
const excludedEmails = "spammer@cal.com, hotmail.com, yahoo, @gmail.com";
const schema = getBookingResponsesSchema({
bookingFields: [
{
name: "name",
type: "name",
required: true,
},
{
name: "email",
type: "email",
required: true,
excludeEmails: excludedEmails,
},
] as z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">,
view: "ALL_VIEWS",
});
const parsedResponses = await schema.safeParseAsync({
name: "test",
email: "harry@gmail.com",
});
expect(parsedResponses.success).toBe(false);
if (parsedResponses.success) {
throw new Error("Should not reach here");
}
expect(parsedResponses.error.issues[0]).toEqual(
expect.objectContaining({
code: "custom",
message: `{email}${CUSTOM_EMAIL_EXCLUDED_ERROR_MSG}`,
})
);
});
test("should pass if the email is not present in excluded emails", async () => {
const excludedEmails = "spammer@cal.com, hotmail.com, yahoo, @gmail.com";
const schema = getBookingResponsesSchema({
bookingFields: [
{
name: "name",
type: "name",
required: true,
},
{
name: "email",
type: "email",
required: true,
excludeEmails: excludedEmails,
},
] as z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">,
view: "ALL_VIEWS",
});
const parsedResponses = await schema.safeParseAsync({
name: "test",
email: "harry@workmail.com",
});
expect(parsedResponses.success).toBe(true);
if (!parsedResponses.success) {
throw new Error("Should not reach here");
}
expect(parsedResponses.data).toEqual({
name: "test",
email: "harry@workmail.com",
});
});
});
@@ -162,13 +162,29 @@ function preprocess<T extends z.ZodType>({
}
if (bookingField.type === "email") {
// Email RegExp to validate if the input is a valid email
if (!bookingField.hidden && !emailSchema.safeParse(value).success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("email_validation_error"),
});
if (!bookingField.hidden) {
// Email RegExp to validate if the input is a valid email
if (!emailSchema.safeParse(value).success) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("email_validation_error"),
});
}
// validate the excluded emails
const bookerEmail = value;
const excludedEmails =
bookingField.excludeEmails?.split(",").map((domain) => domain.trim()) || [];
const match = excludedEmails.find((email) => bookerEmail.includes(email));
if (match) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: m("exclude_emails_match_found_error_message"),
});
}
}
continue;
}
+12 -2
View File
@@ -1,4 +1,5 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import type { SubmitHandler, UseFormReturn } from "react-hook-form";
import { Controller, useFieldArray, useForm, useFormContext } from "react-hook-form";
@@ -32,7 +33,7 @@ import {
import { fieldTypesConfigMap } from "./fieldTypes";
import { fieldsThatSupportLabelAsSafeHtml } from "./fieldsThatSupportLabelAsSafeHtml";
import type { fieldsSchema } from "./schema";
import { fieldSchema, type fieldsSchema } from "./schema";
import { getFieldIdentifier } from "./utils/getFieldIdentifier";
import { getConfig as getVariantsConfig } from "./utils/variantsConfig";
@@ -459,7 +460,7 @@ function FieldEditDialog({
const { t } = useLocale();
const fieldForm = useForm<RhfFormField>({
defaultValues: dialog.data || {},
// resolver: zodResolver(fieldSchema),
resolver: zodResolver(fieldSchema),
});
const formFieldType = fieldForm.getValues("type");
@@ -573,6 +574,15 @@ function FieldEditDialog({
<FieldWithLengthCheckSupport containerClassName="mt-6" fieldForm={fieldForm} />
) : null}
{fieldType.value === "email" && fieldForm.getValues("name") === "email" && (
<InputField
{...fieldForm.register("excludeEmails")}
containerClassName="mt-6"
label={t("exclude_emails_that_contain")}
placeholder="gmail.com, hotmail.com, ..."
/>
)}
<Controller
name="required"
control={fieldForm.control}
+24
View File
@@ -99,6 +99,30 @@ const baseFieldSchema = z.object({
* @requires supportsLengthCheck = true
*/
maxLength: z.number().optional(),
// Emails that needs to be excluded
excludeEmails: z
.string()
.superRefine((val, ctx) => {
const allDomains = val.split(",").map((dom) => dom.trim());
const regex = /^(?:@?[a-z0-9-]+(?:\.[a-z]{2,})?)?(?:@[a-z0-9-]+\.[a-z]{2,})?$/;
/*
Valid patterns - [ example, example.anything, anyone@example.anything ]
Invalid patterns - Patterns involving capital letter [ Example, Example.anything, Anyone@example.anything ]
*/
const isValid = !allDomains.some((domain) => !regex.test(domain));
if (!isValid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Enter valid domain or email",
});
}
})
.optional(),
});
export const variantsConfigSchema = z.object({