Skip to content

Commit

Permalink
feat: error message for invalid variables in custom event name (#7426)
Browse files Browse the repository at this point in the history
* 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 <me@alexvanandel.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
  • Loading branch information
3 people authored Mar 6, 2023
1 parent 8a9b985 commit 3d41c64
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 103 deletions.
144 changes: 144 additions & 0 deletions apps/web/components/eventtype/CustomEventTypeModal.tsx
Original file line number Diff line number Diff line change
@@ -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<CustomEventTypeModalFormProps> = (props) => {
const { t } = useLocale();
const { placeHolder, close, setValue, event } = props;
const { register, handleSubmit, watch, getValues } = useFormContext<FormValues>();
const onSubmit: SubmitHandler<FormValues> = (data) => {
setValue(data.customEventName);
close();
};

// const customEventName = watch("customEventName");
const previewText = getEventName({ ...event, eventName: watch("customEventName") });
const placeHolder_ = watch("customEventName") === "" ? previewText : placeHolder;

return (
<form
id="custom-event-name"
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
const isEmpty = getValues("customEventName") === "";
if (isEmpty) {
setValue("");
}

handleSubmit(onSubmit)(e);
}}>
<TextField
label={t("event_name_in_calendar")}
type="text"
placeholder={placeHolder_}
{...register("customEventName", {
validate: (value) => validateCustomEventName(value, t("invalid_event_name_variables")),
})}
className="mb-0"
/>
<div className="pt-6 text-sm">
<div className="mb-6 rounded-md bg-gray-100 p-2">
<h1 className="mb-2 ml-1 font-medium text-gray-900">{t("available_variables")}</h1>
<div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Event type title}`}</p>
<p className="text-gray-900">{t("event_name_info")}</p>
</div>
<div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Organiser}`}</p>
<p className="text-gray-900">{t("your_full_name")}</p>
</div>
<div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Scheduler}`}</p>
<p className="text-gray-900">{t("scheduler_full_name")}</p>
</div>
<div className="mb-1 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Location}`}</p>
<p className="text-gray-900">{t("location_info")}</p>
</div>
</div>
<h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1>
<div
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center"
style={{
backgroundImage: "url(/calendar-preview.svg)",
}}>
<div className="m-auto flex items-center justify-center self-stretch">
<div className="mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black bg-gray-100 text-[12px] leading-3">
<p className="overflow-hidden text-ellipsis p-1.5 font-medium text-gray-900">{previewText}</p>
<p className="ml-1.5 text-[10px] font-normal text-gray-600">8 - 10 AM</p>
</div>
</div>
</div>
</div>
</form>
);
};

interface CustomEventTypeModalProps {
placeHolder: string;
defaultValue: string;
close: () => void;
setValue: (value: string) => void;
event: EventNameObjectType;
}

const CustomEventTypeModal: FC<CustomEventTypeModalProps> = (props) => {
const { t } = useLocale();

const { defaultValue, placeHolder, close, setValue, event } = props;

const methods = useForm<FormValues>({
defaultValues: {
customEventName: defaultValue,
},
});

return (
<Dialog open={true} onOpenChange={close}>
<DialogContent
title={t("custom_event_name")}
description={t("custom_event_name_description")}
type="creation"
enableOverflow>
<FormProvider {...methods}>
<CustomEventTypeModalForm
event={event}
close={close}
setValue={setValue}
placeHolder={placeHolder}
defaultValue={defaultValue}
/>
</FormProvider>
<DialogFooter>
<DialogClose>{t("cancel")}</DialogClose>

<Button form="custom-event-name" type="submit" color="primary">
{t("create")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

export default CustomEventTypeModal;
122 changes: 21 additions & 101 deletions apps/web/components/eventtype/EventAdvancedTab.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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()}`;
Expand All @@ -53,21 +43,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
host: eventType.users[0]?.name || "Nameless",
t,
};
const [previewText, setPreviewText] = useState(getEventName(eventNameObject));

const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");

const replaceEventNamePlaceholder = (eventNameObject: EventNameObjectType, previewEventName: string) =>
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]);
Expand All @@ -88,8 +68,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
);
};

const eventNamePlaceholder = replaceEventNamePlaceholder(eventNameObject, t("meeting_with_user"));
const eventNamePlaceholder = getEventName({
...eventNameObject,
eventName: formMethods.watch("eventName"),
});

const closeEventNameTip = () => setShowEventNameTip(false);

const setEventName = (value: string) => formMethods.setValue("eventName", value);
return (
<div className="flex flex-col space-y-8">
{/**
Expand Down Expand Up @@ -132,14 +118,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
type="text"
placeholder={eventNamePlaceholder}
defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName", {
onChange: (e) => {
if (!e.target.value || !formMethods.getValues("eventName")) {
return setPreviewText(getEventName(eventNameObject));
}
changePreviewText(eventNameObject, e.target.value);
},
})}
{...formMethods.register("eventName")}
addOnSuffix={
<Button
type="button"
Expand All @@ -148,6 +127,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
color="minimal"
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
onClick={() => setShowEventNameTip((old) => !old)}
aria-label="edit custom name"
/>
}
/>
Expand Down Expand Up @@ -325,73 +305,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
/>

{showEventNameTip && (
<Dialog open={showEventNameTip} onOpenChange={setShowEventNameTip}>
<DialogContent
title={t("custom_event_name")}
description={t("custom_event_name_description")}
type="creation"
enableOverflow>
<TextField
label={t("event_name_in_calendar")}
type="text"
placeholder={eventNamePlaceholder}
defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName", {
onChange: (e) => {
if (!e.target.value || !formMethods.getValues("eventName")) {
return setPreviewText(getEventName(eventNameObject));
}
changePreviewText(eventNameObject, e.target.value);
},
})}
className="mb-0"
/>
<div className="text-sm">
<div className="mb-6 rounded-md bg-gray-100 p-2">
<h1 className="mb-2 ml-1 font-medium text-gray-900">{t("available_variables")}</h1>
<div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Event type title}`}</p>
<p className="text-gray-900">{t("event_name_info")}</p>
</div>
<div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Organiser}`}</p>
<p className="text-gray-900">{t("your_full_name")}</p>
</div>
<div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Scheduler}`}</p>
<p className="text-gray-900">{t("scheduler_full_name")}</p>
</div>
<div className="mb-1 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Location}`}</p>
<p className="text-gray-900">{t("location_info")}</p>
</div>
</div>
<h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1>
<div
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center"
style={{
backgroundImage: "url(/calendar-preview.svg)",
}}>
<div className="m-auto flex items-center justify-center self-stretch">
<div className="mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black bg-gray-100 text-[12px] leading-3">
<p className="overflow-hidden text-ellipsis p-1.5 font-medium text-gray-900">
{previewText}
</p>
<p className="ml-1.5 text-[10px] font-normal text-gray-600">8 - 10 AM</p>
</div>
</div>
</div>
</div>
<DialogFooter>
<DialogClose onClick={() => formMethods.setValue("eventName", eventType.eventName ?? "")}>
{t("cancel")}
</DialogClose>
<Button color="primary" onClick={() => setShowEventNameTip(false)}>
{t("create")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<CustomEventTypeModal
close={closeEventNameTip}
setValue={setEventName}
defaultValue={eventType.eventName || ""}
placeHolder={eventNamePlaceholder}
event={eventNameObject}
/>
)}
</div>
);
Expand Down
7 changes: 7 additions & 0 deletions apps/web/pages/event-types/[type]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
})
Expand Down
2 changes: 1 addition & 1 deletion apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"

}
16 changes: 15 additions & 1 deletion packages/core/event.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TFunction } from "next-i18next";
import type { TFunction } from "next-i18next";

import { guessEventLocationType } from "@calcom/app-store/locations";

Expand Down Expand Up @@ -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;
};

0 comments on commit 3d41c64

Please sign in to comment.