From 099d830e7ef98d133aebebf9436252780c7572ff Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:53:53 +0530 Subject: [PATCH] feat: Add URL-type input in booking question field (#15481) * feat: Add URL-type input in booking question field * fix type error * Update common.json * fix requested changes * small update --------- Co-authored-by: Rajiv Sahal Co-authored-by: Keith Williams --- apps/web/public/static/locales/en/common.json | 1 + .../react-awesome-query-builder/widgets.tsx | 2 +- .../lib/getBookingResponsesSchema.test.ts | 115 ++++++++++++++++++ packages/features/form-builder/Components.tsx | 6 + packages/features/form-builder/fieldTypes.ts | 5 + packages/features/form-builder/propsTypes.ts | 1 + packages/features/form-builder/schema.ts | 17 +++ 7 files changed, 146 insertions(+), 1 deletion(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 394c153528..6985580677 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1128,6 +1128,7 @@ "update_location": "Update Location", "location_updated": "Location updated", "email_validation_error": "That doesn't look like an email address", + "url_validation_error": "That doesn't look like a URL", "place_where_cal_widget_appear": "Place this code in your HTML where you want your {{appName}} widget to appear.", "create_update_react_component": "Create or update an existing React component as shown below.", "copy_code": "Copy Code", diff --git a/packages/app-store/routing-forms/components/react-awesome-query-builder/widgets.tsx b/packages/app-store/routing-forms/components/react-awesome-query-builder/widgets.tsx index 7becdbb4ac..5065df70c7 100644 --- a/packages/app-store/routing-forms/components/react-awesome-query-builder/widgets.tsx +++ b/packages/app-store/routing-forms/components/react-awesome-query-builder/widgets.tsx @@ -70,7 +70,7 @@ export type TextLikeComponentProps = TextLikeComponentProps & { customProps?: object; - type?: "text" | "number" | "email" | "tel"; + type?: "text" | "number" | "email" | "tel" | "url"; maxLength?: number; noLabel?: boolean; }; diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.test.ts b/packages/features/bookings/lib/getBookingResponsesSchema.test.ts index 7ccdc6857b..c40712030c 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.test.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.test.ts @@ -10,6 +10,7 @@ import getBookingResponsesSchema, { getBookingResponsesPartialSchema } from "./g 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 ZOD_REQUIRED_FIELD_ERROR_MSG = "Required"; describe("getBookingResponsesSchema", () => { @@ -994,6 +995,120 @@ describe("getBookingResponsesSchema", () => { test.todo("radio"); test.todo("boolean"); }); +describe("validate url type field", () => { + test(`should fail parsing if invalid url provided`, async ({}) => { + const schema = getBookingResponsesSchema({ + bookingFields: [ + { + name: "name", + type: "name", + required: true, + }, + { + name: "email", + type: "email", + required: true, + }, + { + name: "url", + type: "url", + required: true, + }, + ] as z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">, + view: "ALL_VIEWS", + }); + const parsedResponses = await schema.safeParseAsync({ + email: "test@test.com", + name: "test", + url: "www.example.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: `{url}${CUSTOM_URL_VALIDATION_ERROR_MSG}`, + }) + ); + }); + test(`should successfully give responses if url type field value is valid`, async ({}) => { + const schema = getBookingResponsesSchema({ + bookingFields: [ + { + name: "name", + type: "name", + required: true, + }, + { + name: "email", + type: "email", + required: true, + }, + { + name: "url", + type: "url", + required: true, + }, + ] as z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">, + view: "ALL_VIEWS", + }); + const parsedResponses = await schema.safeParseAsync({ + email: "test@test.com", + name: "test", + url: "https://8x8.vc/company", + }); + expect(parsedResponses.success).toBe(true); + if (!parsedResponses.success) { + throw new Error("Should not reach here"); + } + expect(parsedResponses.data).toEqual({ + email: "test@test.com", + name: "test", + url: "https://8x8.vc/company", + }); + }); + + test("should fail parsing if url field value is empty", async ({}) => { + const schema = getBookingResponsesSchema({ + bookingFields: [ + { + name: "name", + type: "name", + required: true, + }, + { + name: "email", + type: "email", + required: true, + }, + { + name: "url", + type: "url", + required: true, + }, + ] as z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">, + view: "ALL_VIEWS", + }); + const parsedResponses = await schema.safeParseAsync({ + email: "test@test.com", + name: "test", + url: "", + }); + expect(parsedResponses.success).toBe(false); + // eslint-disable-next-line playwright/no-conditional-in-test + if (parsedResponses.success) { + throw new Error("Should not reach here"); + } + expect(parsedResponses.error.issues[0]).toEqual( + expect.objectContaining({ + code: "custom", + message: `{url}${CUSTOM_REQUIRED_FIELD_ERROR_MSG}`, + }) + ); + }); +}); describe("getBookingResponsesPartialSchema - Prefill validation", () => { test(`should be able to get fields prefilled even when name is empty string`, async ({}) => { diff --git a/packages/features/form-builder/Components.tsx b/packages/features/form-builder/Components.tsx index dc0109ec59..021f54325f 100644 --- a/packages/features/form-builder/Components.tsx +++ b/packages/features/form-builder/Components.tsx @@ -512,5 +512,11 @@ export const Components: Record = { ); }, }, + url: { + propsType: propsTypes.url, + factory: (props) => { + return ; + }, + }, } as const; // Should use `statisfies` to check if the `type` is from supported types. But satisfies doesn't work with Next.js config diff --git a/packages/features/form-builder/fieldTypes.ts b/packages/features/form-builder/fieldTypes.ts index e4606ff831..936200d1af 100644 --- a/packages/features/form-builder/fieldTypes.ts +++ b/packages/features/form-builder/fieldTypes.ts @@ -152,6 +152,11 @@ const configMap: Record, " value: "boolean", isTextType: false, }, + url: { + label: "URL", + value: "url", + isTextType: true, + }, }; export const fieldTypesConfigMap = configMap as Record>; diff --git a/packages/features/form-builder/propsTypes.ts b/packages/features/form-builder/propsTypes.ts index 8bfcb0bc20..9a479959e6 100644 --- a/packages/features/form-builder/propsTypes.ts +++ b/packages/features/form-builder/propsTypes.ts @@ -13,4 +13,5 @@ export const propsTypes = { checkbox: "multiselect", radio: "select", boolean: "boolean", + url: "text", } as const; diff --git a/packages/features/form-builder/schema.ts b/packages/features/form-builder/schema.ts index f94d145b93..0d9c275006 100644 --- a/packages/features/form-builder/schema.ts +++ b/packages/features/form-builder/schema.ts @@ -23,6 +23,7 @@ const fieldTypeEnum = z.enum([ "radio", "radioInput", "boolean", + "url", ]); export type FieldType = z.infer; @@ -368,6 +369,22 @@ export const fieldTypesSchemaMap: Partial< } }, }, + url: { + preprocess: ({ response }) => { + return response.trim(); + }, + superRefine: ({ response, ctx, m }) => { + const value = response ?? ""; + const urlSchema = z.string().url(); + + if (!urlSchema.safeParse(value).success) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: m("url_validation_error"), + }); + } + }, + }, }; /**