fix: Make identifier conform to RHF field requirement (#10860)

Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
This commit is contained in:
Hariom Balhara 2023-08-21 22:41:47 +05:30 committed by GitHub
parent 7a7a5b5285
commit 51caa6834a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 117 additions and 17 deletions

View File

@ -202,7 +202,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
await addQuestionAndSave({ await addQuestionAndSave({
page, page,
question: { question: {
name: "how_are_you", name: "how-are-you",
type: "Address", type: "Address",
label: "How are you?", label: "How are you?",
placeholder: "I'm fine, thanks", placeholder: "I'm fine, thanks",
@ -214,7 +214,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
const allFieldsLocator = await expectSystemFieldsToBeThereOnBookingPage({ page }); const allFieldsLocator = await expectSystemFieldsToBeThereOnBookingPage({ page });
const userFieldLocator = allFieldsLocator.nth(5); const userFieldLocator = allFieldsLocator.nth(5);
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible(); await expect(userFieldLocator.locator('[name="how-are-you"]')).toBeVisible();
// There are 2 labels right now. Will be one in future. The second one is hidden // There are 2 labels right now. Will be one in future. The second one is hidden
expect(await getLabelText(userFieldLocator)).toBe("How are you?"); expect(await getLabelText(userFieldLocator)).toBe("How are you?");
await expect(userFieldLocator.locator("input")).toBeVisible(); await expect(userFieldLocator.locator("input")).toBeVisible();
@ -223,18 +223,18 @@ async function runTestStepsCommonForTeamAndUserEventType(
await test.step("Hide Question and see that it's not shown on Booking Page", async () => { await test.step("Hide Question and see that it's not shown on Booking Page", async () => {
await toggleQuestionAndSave({ await toggleQuestionAndSave({
name: "how_are_you", name: "how-are-you",
page, page,
}); });
await doOnFreshPreview(page, context, async (page) => { await doOnFreshPreview(page, context, async (page) => {
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]'); const formBuilderFieldLocator = page.locator('[data-fob-field-name="how-are-you"]');
await expect(formBuilderFieldLocator).toBeHidden(); await expect(formBuilderFieldLocator).toBeHidden();
}); });
}); });
await test.step("Show Question Again", async () => { await test.step("Show Question Again", async () => {
await toggleQuestionAndSave({ await toggleQuestionAndSave({
name: "how_are_you", name: "how-are-you",
page, page,
}); });
}); });
@ -242,7 +242,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
await test.step('Try to book without providing "How are you?" response', async () => { await test.step('Try to book without providing "How are you?" response', async () => {
await doOnFreshPreview(page, context, async (page) => { await doOnFreshPreview(page, context, async (page) => {
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" }); await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
await expectErrorToBeThereFor({ page, name: "how_are_you" }); await expectErrorToBeThereFor({ page, name: "how-are-you" });
}); });
}); });
@ -260,18 +260,18 @@ async function runTestStepsCommonForTeamAndUserEventType(
page, page,
context, context,
async (page) => { async (page) => {
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]'); const formBuilderFieldLocator = page.locator('[data-fob-field-name="how-are-you"]');
await expect(formBuilderFieldLocator).toBeVisible(); await expect(formBuilderFieldLocator).toBeVisible();
expect( expect(
await formBuilderFieldLocator.locator('[name="how_are_you"]').getAttribute("placeholder") await formBuilderFieldLocator.locator('[name="how-are-you"]').getAttribute("placeholder")
).toBe("I'm fine, thanks"); ).toBe("I'm fine, thanks");
expect(await getLabelText(formBuilderFieldLocator)).toBe("How are you?"); expect(await getLabelText(formBuilderFieldLocator)).toBe("How are you?");
await formBuilderFieldLocator.locator('[name="how_are_you"]').fill("I am great!"); await formBuilderFieldLocator.locator('[name="how-are-you"]').fill("I am great!");
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" }); await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
await expect(page.locator("[data-testid=success-page]")).toBeVisible(); await expect(page.locator("[data-testid=success-page]")).toBeVisible();
expect( expect(
await page.locator('[data-testid="field-response"][data-fob-field="how_are_you"]').innerText() await page.locator('[data-testid="field-response"][data-fob-field="how-are-you"]').innerText()
).toBe("I am great!"); ).toBe("I am great!");
await waitFor(() => { await waitFor(() => {
@ -287,7 +287,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
label: "email_address", label: "email_address",
value: "booker@example.com", value: "booker@example.com",
}, },
how_are_you: { "how-are-you": {
label: "How are you?", label: "How are you?",
value: "I am great!", value: "I am great!",
}, },
@ -303,7 +303,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
}); });
expect(payload.userFieldsResponses).toMatchObject({ expect(payload.userFieldsResponses).toMatchObject({
how_are_you: { "how-are-you": {
label: "How are you?", label: "How are you?",
value: "I am great!", value: "I am great!",
}, },

View File

@ -3,6 +3,7 @@ import type { z } from "zod";
import { SMS_REMINDER_NUMBER_FIELD } from "@calcom/features/bookings/lib/SystemField"; import { SMS_REMINDER_NUMBER_FIELD } from "@calcom/features/bookings/lib/SystemField";
import { fieldsThatSupportLabelAsSafeHtml } from "@calcom/features/form-builder/fieldsThatSupportLabelAsSafeHtml"; import { fieldsThatSupportLabelAsSafeHtml } from "@calcom/features/form-builder/fieldsThatSupportLabelAsSafeHtml";
import { getFieldIdentifier } from "@calcom/features/form-builder/utils/getFieldIdentifier";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import slugify from "@calcom/lib/slugify"; import slugify from "@calcom/lib/slugify";
import { EventTypeCustomInputType } from "@calcom/prisma/enums"; import { EventTypeCustomInputType } from "@calcom/prisma/enums";
@ -285,7 +286,9 @@ export const ensureBookingInputsHaveSystemFields = ({
const missingSystemBeforeFields = []; const missingSystemBeforeFields = [];
for (const field of systemBeforeFields) { for (const field of systemBeforeFields) {
const existingBookingFieldIndex = bookingFields.findIndex((f) => f.name === field.name); const existingBookingFieldIndex = bookingFields.findIndex(
(f) => getFieldIdentifier(f.name) === getFieldIdentifier(field.name)
);
// Only do a push, we must not update existing system fields as user could have modified any property in it, // Only do a push, we must not update existing system fields as user could have modified any property in it,
if (existingBookingFieldIndex === -1) { if (existingBookingFieldIndex === -1) {
missingSystemBeforeFields.push(field); missingSystemBeforeFields.push(field);
@ -303,8 +306,13 @@ export const ensureBookingInputsHaveSystemFields = ({
// Backward Compatibility for SMS Reminder Number // Backward Compatibility for SMS Reminder Number
// Note: We still need workflows in `getBookingFields` due to Backward Compatibility. If we do a one time entry for all event-types, we can remove workflows from `getBookingFields` // Note: We still need workflows in `getBookingFields` due to Backward Compatibility. If we do a one time entry for all event-types, we can remove workflows from `getBookingFields`
// Also, note that even if Workflows don't explicity add smsReminderNumber field to bookingFields, it would be added as a side effect of this backward compatibility logic // Also, note that even if Workflows don't explicity add smsReminderNumber field to bookingFields, it would be added as a side effect of this backward compatibility logic
if (smsNumberSources.length && !bookingFields.find((f) => f.name !== SMS_REMINDER_NUMBER_FIELD)) { if (
const indexForLocation = bookingFields.findIndex((f) => f.name === "location"); smsNumberSources.length &&
!bookingFields.find((f) => getFieldIdentifier(f.name) !== getFieldIdentifier(SMS_REMINDER_NUMBER_FIELD))
) {
const indexForLocation = bookingFields.findIndex(
(f) => getFieldIdentifier(f.name) === getFieldIdentifier("location")
);
// Add the SMS Reminder Number field after `location` field always // Add the SMS Reminder Number field after `location` field always
bookingFields.splice(indexForLocation + 1, 0, { bookingFields.splice(indexForLocation + 1, 0, {
...getSmsReminderNumberField(), ...getSmsReminderNumberField(),
@ -340,7 +348,9 @@ export const ensureBookingInputsHaveSystemFields = ({
const missingSystemAfterFields = []; const missingSystemAfterFields = [];
for (const field of systemAfterFields) { for (const field of systemAfterFields) {
const existingBookingFieldIndex = bookingFields.findIndex((f) => f.name === field.name); const existingBookingFieldIndex = bookingFields.findIndex(
(f) => getFieldIdentifier(f.name) === getFieldIdentifier(field.name)
);
// Only do a push, we must not update existing system fields as user could have modified any property in it, // Only do a push, we must not update existing system fields as user could have modified any property in it,
if (existingBookingFieldIndex === -1) { if (existingBookingFieldIndex === -1) {
missingSystemAfterFields.push(field); missingSystemAfterFields.push(field);

View File

@ -30,6 +30,7 @@ import { fieldTypesConfigMap } from "./fieldTypes";
import { fieldsThatSupportLabelAsSafeHtml } from "./fieldsThatSupportLabelAsSafeHtml"; import { fieldsThatSupportLabelAsSafeHtml } from "./fieldsThatSupportLabelAsSafeHtml";
import type { fieldsSchema } from "./schema"; import type { fieldsSchema } from "./schema";
import { getVariantsConfig } from "./utils"; import { getVariantsConfig } from "./utils";
import { getFieldIdentifier } from "./utils/getFieldIdentifier";
type RhfForm = { type RhfForm = {
fields: z.infer<typeof fieldsSchema>; fields: z.infer<typeof fieldsSchema>;
@ -448,6 +449,9 @@ function FieldEditDialog({
required required
{...fieldForm.register("name")} {...fieldForm.register("name")}
containerClassName="mt-6" containerClassName="mt-6"
onChange={(e) => {
fieldForm.setValue("name", getFieldIdentifier(e.target.value || ""));
}}
disabled={ disabled={
fieldForm.getValues("editable") === "system" || fieldForm.getValues("editable") === "system" ||
fieldForm.getValues("editable") === "system-but-optional" fieldForm.getValues("editable") === "system-but-optional"

View File

@ -1,5 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { getValidRhfFieldName } from "@calcom/lib/getValidRhfFieldName";
import { fieldTypesConfigMap } from "./fieldTypes"; import { fieldTypesConfigMap } from "./fieldTypes";
import { getVariantsConfig, preprocessNameFieldDataWithVariant } from "./utils"; import { getVariantsConfig, preprocessNameFieldDataWithVariant } from "./utils";
@ -31,7 +33,7 @@ export const EditableSchema = z.enum([
]); ]);
const baseFieldSchema = z.object({ const baseFieldSchema = z.object({
name: z.string(), name: z.string().transform(getValidRhfFieldName),
type: fieldTypeEnum, type: fieldTypeEnum,
// TODO: We should make at least one of `defaultPlaceholder` and `placeholder` required. Do the same for label. // TODO: We should make at least one of `defaultPlaceholder` and `placeholder` required. Do the same for label.
label: z.string().optional(), label: z.string().optional(),

View File

@ -0,0 +1,5 @@
import { getValidRhfFieldName } from "@calcom/lib/getValidRhfFieldName";
export const getFieldIdentifier = (name: string) => {
return getValidRhfFieldName(name);
};

View File

@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { getValidRhfFieldName } from "./getValidRhfFieldName";
describe("getValidRhfFieldName", () => {
it("should not convert to lowercase", () => {
expect(getValidRhfFieldName("Hello")).toEqual("Hello");
expect(getValidRhfFieldName("HELLO")).toEqual("HELLO");
});
it("should convert spaces, _, and any other special character to -", () => {
expect(getValidRhfFieldName("hello there")).toEqual("hello-there");
expect(getValidRhfFieldName("hello_there")).toEqual("hello-there");
expect(getValidRhfFieldName("hello$there")).toEqual("hello-there");
expect(getValidRhfFieldName("$hello$there")).toEqual("-hello-there");
expect(getValidRhfFieldName("$hello.there")).toEqual("-hello-there");
});
// So that user can freely add spaces and any other character iteratively and it get's converted to - and he can add more characters.
// We don't really care about a hyphen in the end
it("should not remove dashes from start and end.", () => {
expect(getValidRhfFieldName("hello-there-")).toEqual("hello-there-");
expect(getValidRhfFieldName("hello-there_")).toEqual("hello-there-");
expect(getValidRhfFieldName("_hello-there_")).toEqual("-hello-there-");
expect(getValidRhfFieldName("$hello-there_")).toEqual("-hello-there-");
});
it("should remove unicode and emoji characters", () => {
expect(getValidRhfFieldName("Hello 📚🕯️®️ There")).toEqual("Hello---------There");
expect(getValidRhfFieldName("📚🕯️®️")).toEqual("-------");
});
it("should keep numbers as is", () => {
expect(getValidRhfFieldName("hellothere123")).toEqual("hellothere123");
expect(getValidRhfFieldName("321hello there123")).toEqual("321hello-there123");
expect(getValidRhfFieldName("hello$there")).toEqual("hello-there");
});
});

View File

@ -0,0 +1,5 @@
export const getValidRhfFieldName = (fieldName: string) => {
// Remember that any transformation that you do here would run on System Field names as well. So, be careful and avoiding doing anything here that would modify the SystemField names.
// e.g. SystemField name currently have uppercases in them. So, no need to lowercase unless absolutely needed.
return fieldName.replace(/[^a-zA-Z0-9]/g, "-");
};

View File

@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { slugify } from "./slugify";
describe("slugify", () => {
it("should convert to lowercase", () => {
expect(slugify("Hello")).toEqual("hello");
expect(slugify("HELLO")).toEqual("hello");
});
it("should convert spaces, _, and any other special character to -", () => {
expect(slugify("hello there")).toEqual("hello-there");
expect(slugify("hello_there")).toEqual("hello-there");
expect(slugify("hello$there")).toEqual("hello-there");
});
it("should keep numbers as is", () => {
expect(slugify("hellothere123")).toEqual("hellothere123");
expect(slugify("321hello there123")).toEqual("321hello-there123");
expect(slugify("hello$there")).toEqual("hello-there");
});
// So that user can freely add spaces and any other character iteratively and it get's converted to - later on.
it("should remove dashes from start and end.", () => {
expect(slugify("hello-there-")).toEqual("hello-there");
expect(slugify("hello-there_")).toEqual("hello-there");
expect(slugify("_hello-there_")).toEqual("hello-there");
expect(slugify("$hello-there_")).toEqual("hello-there");
});
// This is failing, if we want to fix it, one approach is as used in getValidRhfFieldName
it.skip("should remove unicode and emoji characters", () => {
expect(slugify("Hello 📚🕯️®️ There")).toEqual("hello---------there");
expect(slugify("📚🕯️®️")).toEqual("");
});
});