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 <sahalrajiv6900@gmail.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
co-authored by
Rajiv Sahal
Keith Williams
parent
add59896f5
commit
099d830e7e
@@ -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",
|
||||
|
||||
@@ -70,7 +70,7 @@ export type TextLikeComponentProps<TVal extends string | string[] | boolean = st
|
||||
export type TextLikeComponentPropsRAQB<TVal extends string | boolean = string> =
|
||||
TextLikeComponentProps<TVal> & {
|
||||
customProps?: object;
|
||||
type?: "text" | "number" | "email" | "tel";
|
||||
type?: "text" | "number" | "email" | "tel" | "url";
|
||||
maxLength?: number;
|
||||
noLabel?: boolean;
|
||||
};
|
||||
|
||||
@@ -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<typeof eventTypeBookingFields> & 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<typeof eventTypeBookingFields> & 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<typeof eventTypeBookingFields> & 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 ({}) => {
|
||||
|
||||
@@ -512,5 +512,11 @@ export const Components: Record<FieldType, Component> = {
|
||||
);
|
||||
},
|
||||
},
|
||||
url: {
|
||||
propsType: propsTypes.url,
|
||||
factory: (props) => {
|
||||
return <Widgets.TextWidget type="url" noLabel={true} {...props} />;
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
// Should use `statisfies` to check if the `type` is from supported types. But satisfies doesn't work with Next.js config
|
||||
|
||||
@@ -152,6 +152,11 @@ const configMap: Record<FieldType, Omit<z.infer<typeof fieldTypeConfigSchema>, "
|
||||
value: "boolean",
|
||||
isTextType: false,
|
||||
},
|
||||
url: {
|
||||
label: "URL",
|
||||
value: "url",
|
||||
isTextType: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const fieldTypesConfigMap = configMap as Record<FieldType, z.infer<typeof fieldTypeConfigSchema>>;
|
||||
|
||||
@@ -13,4 +13,5 @@ export const propsTypes = {
|
||||
checkbox: "multiselect",
|
||||
radio: "select",
|
||||
boolean: "boolean",
|
||||
url: "text",
|
||||
} as const;
|
||||
|
||||
@@ -23,6 +23,7 @@ const fieldTypeEnum = z.enum([
|
||||
"radio",
|
||||
"radioInput",
|
||||
"boolean",
|
||||
"url",
|
||||
]);
|
||||
|
||||
export type FieldType = z.infer<typeof fieldTypeEnum>;
|
||||
@@ -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"),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user