From 3d41c64d8125841582511ff7972d5fa9416be463 Mon Sep 17 00:00:00 2001 From: Nafees Nazik <84864519+G3root@users.noreply.github.com> Date: Mon, 6 Mar 2023 17:44:54 +0530 Subject: [PATCH] feat: error message for invalid variables in custom event name (#7426) * feat: add custom validate name util * refactor: separate custom event type modal into a different component * feat: add validation to zod * chore: add i18n key * feat: add dynamic imports * fix: padding * Omit cache-hit exit 1, assuming it'll fail regardless --------- Co-authored-by: Alex van Andel Co-authored-by: CarinaWolli --- .../eventtype/CustomEventTypeModal.tsx | 144 ++++++++++++++++++ .../components/eventtype/EventAdvancedTab.tsx | 122 +++------------ apps/web/pages/event-types/[type]/index.tsx | 7 + apps/web/public/static/locales/en/common.json | 2 +- packages/core/event.ts | 16 +- 5 files changed, 188 insertions(+), 103 deletions(-) create mode 100644 apps/web/components/eventtype/CustomEventTypeModal.tsx diff --git a/apps/web/components/eventtype/CustomEventTypeModal.tsx b/apps/web/components/eventtype/CustomEventTypeModal.tsx new file mode 100644 index 00000000000000..aa7984d94b96cc --- /dev/null +++ b/apps/web/components/eventtype/CustomEventTypeModal.tsx @@ -0,0 +1,144 @@ +import type { FC } from "react"; +import type { SubmitHandler } from "react-hook-form"; +import { FormProvider } from "react-hook-form"; +import { useForm, useFormContext } from "react-hook-form"; + +import type { EventNameObjectType } from "@calcom/core/event"; +import { getEventName } from "@calcom/core/event"; +import { validateCustomEventName } from "@calcom/core/event"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Button, Dialog, DialogClose, DialogFooter, DialogContent, TextField } from "@calcom/ui"; + +interface FormValues { + customEventName: string; +} + +interface CustomEventTypeModalFormProps { + placeHolder: string; + close: () => void; + setValue: (value: string) => void; + event: EventNameObjectType; + defaultValue: string; +} + +const CustomEventTypeModalForm: FC = (props) => { + const { t } = useLocale(); + const { placeHolder, close, setValue, event } = props; + const { register, handleSubmit, watch, getValues } = useFormContext(); + const onSubmit: SubmitHandler = (data) => { + setValue(data.customEventName); + close(); + }; + + // const customEventName = watch("customEventName"); + const previewText = getEventName({ ...event, eventName: watch("customEventName") }); + const placeHolder_ = watch("customEventName") === "" ? previewText : placeHolder; + + return ( +
{ + e.preventDefault(); + e.stopPropagation(); + const isEmpty = getValues("customEventName") === ""; + if (isEmpty) { + setValue(""); + } + + handleSubmit(onSubmit)(e); + }}> + validateCustomEventName(value, t("invalid_event_name_variables")), + })} + className="mb-0" + /> +
+
+

{t("available_variables")}

+
+

{`{Event type title}`}

+

{t("event_name_info")}

+
+
+

{`{Organiser}`}

+

{t("your_full_name")}

+
+
+

{`{Scheduler}`}

+

{t("scheduler_full_name")}

+
+
+

{`{Location}`}

+

{t("location_info")}

+
+
+

{t("preview")}

+
+
+
+

{previewText}

+

8 - 10 AM

+
+
+
+
+ + ); +}; + +interface CustomEventTypeModalProps { + placeHolder: string; + defaultValue: string; + close: () => void; + setValue: (value: string) => void; + event: EventNameObjectType; +} + +const CustomEventTypeModal: FC = (props) => { + const { t } = useLocale(); + + const { defaultValue, placeHolder, close, setValue, event } = props; + + const methods = useForm({ + defaultValues: { + customEventName: defaultValue, + }, + }); + + return ( + + + + + + + {t("cancel")} + + + + + + ); +}; + +export default CustomEventTypeModal; diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx index f3abfca726ba13..e23389d0d3f5ae 100644 --- a/apps/web/components/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/eventtype/EventAdvancedTab.tsx @@ -1,3 +1,4 @@ +import dynamic from "next/dynamic"; import Link from "next/link"; import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { useEffect, useState } from "react"; @@ -12,24 +13,13 @@ import { FormBuilder } from "@calcom/features/form-builder/FormBuilder"; import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; -import { - Badge, - Button, - Checkbox, - Dialog, - DialogClose, - DialogContent, - DialogFooter, - Label, - SettingsToggle, - showToast, - TextField, - Tooltip, -} from "@calcom/ui"; +import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui"; import { FiEdit, FiCopy } from "@calcom/ui/components/icon"; import RequiresConfirmationController from "./RequiresConfirmationController"; +const CustomEventTypeModal = dynamic(() => import("@components/eventtype/CustomEventTypeModal")); + const generateHashedLink = (id: number) => { const translator = short(); const seed = `${id}:${new Date().getTime()}`; @@ -53,21 +43,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick - previewEventName - .replace("{Event type title}", eventNameObject.eventType) - .replace("{Scheduler}", eventNameObject.attendeeName) - .replace("{Organiser}", eventNameObject.host); - - const changePreviewText = (eventNameObject: EventNameObjectType, previewEventName: string) => { - setPreviewText(replaceEventNamePlaceholder(eventNameObject, previewEventName)); - }; - useEffect(() => { !hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id)); }, [eventType.users, hashedUrl, team?.id]); @@ -88,8 +68,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick setShowEventNameTip(false); + + const setEventName = (value: string) => formMethods.setValue("eventName", value); return (
{/** @@ -132,14 +118,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick { - if (!e.target.value || !formMethods.getValues("eventName")) { - return setPreviewText(getEventName(eventNameObject)); - } - changePreviewText(eventNameObject, e.target.value); - }, - })} + {...formMethods.register("eventName")} addOnSuffix={ - - - + )}
); diff --git a/apps/web/pages/event-types/[type]/index.tsx b/apps/web/pages/event-types/[type]/index.tsx index 5808efe651d179..f54f839202ac13 100644 --- a/apps/web/pages/event-types/[type]/index.tsx +++ b/apps/web/pages/event-types/[type]/index.tsx @@ -8,6 +8,7 @@ import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; +import { validateCustomEventName } from "@calcom/core/event"; import type { EventLocationType } from "@calcom/core/location"; import { validateBookingLimitOrder } from "@calcom/lib"; import { CAL_URL } from "@calcom/lib/constants"; @@ -218,6 +219,12 @@ const EventTypePage = (props: EventTypeSetupProps) => { .object({ // Length if string, is converted to a number or it can be a number // Make it optional because it's not submitted from all tabs of the page + eventName: z + .string() + .refine((val) => validateCustomEventName(val, t("invalid_event_name_variables")) === true, { + message: t("invalid_event_name_variables"), + }) + .optional(), length: z.union([z.string().transform((val) => +val), z.number()]).optional(), bookingFields: eventTypeBookingFields, }) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 5298332ce3d5f1..c32e98f4b6d9f4 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1635,8 +1635,8 @@ "this_will_be_the_placeholder": "This will be the placeholder", "verification_code": "Verification code", "verify": "Verify", + "invalid_event_name_variables": "There is an invalid variable in your event name", "select_all":"Select All", "default_conferncing_bulk_title":"Bulk update existing event types", "default_conferncing_bulk_description":"Update the locations for the selected event types" - } diff --git a/packages/core/event.ts b/packages/core/event.ts index bb5cb2b3cad63c..adea9b7eaeef8a 100644 --- a/packages/core/event.ts +++ b/packages/core/event.ts @@ -1,4 +1,4 @@ -import { TFunction } from "next-i18next"; +import type { TFunction } from "next-i18next"; import { guessEventLocationType } from "@calcom/app-store/locations"; @@ -43,3 +43,17 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView .replace("{HOST/ATTENDEE}", forAttendeeView ? eventNameObj.host : eventNameObj.attendeeName) ); } + +export const validateCustomEventName = (value: string, message: string) => { + const validVariables = ["{Event type title}", "{Organiser}", "{Scheduler}", "{Location}"]; + const matches = value.match(/\{([^}]+)\}/g); + if (matches?.length) { + for (const item of matches) { + if (!validVariables.includes(item)) { + return message; + } + } + } + + return true; +};