fix: UI mismatch with form state (#10651)

* fix: UI mismatch with form state

* Fix: Sync form state with ui for more fields & slight code refactor

* Ensure Stripe app is enabled to disable seats

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
This commit is contained in:
Mehul
2024-01-24 10:55:31 +00:00
committed by GitHub
co-authored by Udit Takkar Hariom Joe Au-Yeung Joe Au-Yeung
parent 3b05ebc5b5
commit 0a2e275908
11 changed files with 235 additions and 251 deletions
@@ -55,8 +55,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const { t } = useLocale();
const [showEventNameTip, setShowEventNameTip] = useState(false);
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!formMethods.getValues("hashedLink"));
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!formMethods.getValues("successRedirectUrl"));
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
const bookingFields: Prisma.JsonObject = {};
@@ -66,27 +66,31 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
user?.theme === "dark" ||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
eventType.bookingFields.forEach(({ name }) => {
formMethods.getValues().bookingFields.forEach(({ name }) => {
bookingFields[name] = `${name} input`;
});
const eventNameObject: EventNameObjectType = {
attendeeName: t("scheduler"),
eventType: eventType.title,
eventName: eventType.eventName,
host: eventType.users[0]?.name || "Nameless",
eventType: formMethods.getValues("title"),
eventName: formMethods.getValues("eventName"),
host: formMethods.getValues("users")[0]?.name || "Nameless",
bookingFields: bookingFields,
t,
};
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const [requiresConfirmation, setRequiresConfirmation] = useState(
formMethods.getValues("requiresConfirmation")
);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
const noShowFeeEnabled = eventType.metadata?.apps?.stripe?.paymentOption === "HOLD";
const noShowFeeEnabled =
formMethods.getValues("metadata")?.apps?.stripe?.enabled === true &&
formMethods.getValues("metadata")?.apps?.stripe?.paymentOption === "HOLD";
useEffect(() => {
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
}, [eventType.users, hashedUrl, team?.id]);
!hashedUrl && setHashedUrl(generateHashedLink(formMethods.getValues("users")[0]?.id ?? team?.id));
}, [formMethods.getValues("users"), hashedUrl, team?.id]);
const toggleGuests = (enabled: boolean) => {
const bookingFields = formMethods.getValues("bookingFields");
@@ -108,7 +112,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
};
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
formMethods.getValues(),
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
@@ -122,7 +126,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const closeEventNameTip = () => setShowEventNameTip(false);
const setEventName = (value: string) => formMethods.setValue("eventName", value);
return (
<div className="flex flex-col space-y-4">
{/**
@@ -145,12 +148,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
</Link>
</div>
<Controller
control={formMethods.control}
name="destinationCalendar"
defaultValue={eventType.destinationCalendar || undefined}
render={({ field: { onChange, value } }) => (
<DestinationCalendarSelector
destinationCalendar={eventType.destinationCalendar}
value={value ? value.externalId : undefined}
onChange={onChange}
hidePlaceholder
@@ -167,7 +167,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
type="text"
{...shouldLockDisableProps("eventName")}
placeholder={eventNamePlaceholder}
defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")}
addOnSuffix={
<Button
@@ -194,7 +193,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
{...shouldLockDisableProps("bookingFields")}
dataStore={{
options: {
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
locations: getLocationsOptionsForSelect(formMethods.getValues("locations") ?? [], t),
},
}}
/>
@@ -203,15 +202,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<RequiresConfirmationController
eventType={eventType}
seatsEnabled={seatsEnabled}
metadata={eventType.metadata}
metadata={formMethods.getValues("metadata")}
requiresConfirmation={requiresConfirmation}
onRequiresConfirmation={setRequiresConfirmation}
/>
<Controller
name="requiresBookerEmailVerification"
control={formMethods.control}
defaultValue={eventType.requiresBookerEmailVerification}
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName="text-sm"
@@ -228,8 +225,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Controller
name="hideCalendarNotes"
control={formMethods.control}
defaultValue={eventType.hideCalendarNotes}
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName="text-sm"
@@ -246,7 +241,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Controller
name="successRedirectUrl"
control={formMethods.control}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
@@ -274,7 +268,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
placeholder={t("external_redirect_url")}
required={redirectUrlVisible}
type="text"
defaultValue={eventType.successRedirectUrl || ""}
{...formMethods.register("successRedirectUrl")}
/>
<div
@@ -327,7 +320,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
hint={t("private_link_hint")}
defaultValue={placeholderHashedLink}
addOnSuffix={
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
<Tooltip
content={
formMethods.getValues("hashedLink") ? t("copy_to_clipboard") : t("enabled_after_update")
}>
<Button
color="minimal"
size="sm"
@@ -336,7 +332,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
aria-label="copy link"
onClick={() => {
navigator.clipboard.writeText(placeholderHashedLink);
if (eventType.hashedLink) {
if (formMethods.getValues("hashedLink")) {
showToast(t("private_link_copied"), "success");
} else {
showToast(t("enabled_after_update_description"), "warning");
@@ -353,8 +349,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Controller
name="seatsPerTimeSlotEnabled"
control={formMethods.control}
defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
@@ -378,7 +372,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
formMethods.setValue("requiresConfirmation", false);
setRequiresConfirmation(false);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("seatsPerTimeSlot", 2);
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2);
} else {
formMethods.setValue("seatsPerTimeSlot", null);
toggleGuests(true);
@@ -388,8 +382,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
<Controller
name="seatsPerTimeSlot"
control={formMethods.control}
defaultValue={eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<div>
<TextField
@@ -399,7 +391,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
label={t("number_of_seats")}
type="number"
disabled={seatsLocked.disabled}
defaultValue={value || 2}
defaultValue={value}
min={1}
containerClassName="max-w-80"
addOnSuffix={<>{t("seats")}</>}
@@ -408,21 +400,29 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
}}
/>
<div className="mt-4">
<CheckboxField
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
defaultChecked={!!eventType.seatsShowAttendees}
<Controller
name="seatsShowAttendees"
render={({ field: { value, onChange } }) => (
<CheckboxField
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => onChange(e)}
checked={value}
/>
)}
/>
</div>
<div className="mt-2">
<CheckboxField
description={t("show_available_seats_count")}
disabled={seatsLocked.disabled}
onChange={(e) =>
formMethods.setValue("seatsShowAvailabilityCount", e.target.checked)
}
defaultChecked={!!eventType.seatsShowAvailabilityCount}
<Controller
name="seatsShowAvailabilityCount"
render={({ field: { value, onChange } }) => (
<CheckboxField
description={t("show_available_seats_count")}
disabled={seatsLocked.disabled}
onChange={(e) => onChange(e)}
checked={value}
/>
)}
/>
</div>
</div>
@@ -436,8 +436,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
/>
<Controller
name="lockTimeZoneToggleOnBookingPage"
control={formMethods.control}
defaultValue={eventType.lockTimeZoneToggleOnBookingPage}
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName="text-sm"
@@ -452,59 +450,47 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
)}
/>
{allowDisablingAttendeeConfirmationEmails(workflows) && (
<>
<Controller
name="metadata.disableStandardEmails.confirmation.attendee"
control={formMethods.control}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_attendees_confirmation_emails")}
description={t("disable_attendees_confirmation_emails_description")}
checked={value || false}
onCheckedChange={(e) => {
formMethods.setValue("metadata.disableStandardEmails.confirmation.attendee", e);
onChange(e);
}}
/>
</>
)}
/>
</>
<Controller
name="metadata.disableStandardEmails.confirmation.attendee"
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_attendees_confirmation_emails")}
description={t("disable_attendees_confirmation_emails_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
</>
)}
/>
)}
{allowDisablingHostConfirmationEmails(workflows) && (
<>
<Controller
name="metadata.disableStandardEmails.confirmation.host"
control={formMethods.control}
defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_host_confirmation_emails")}
description={t("disable_host_confirmation_emails_description")}
checked={value || false}
onCheckedChange={(e) => {
formMethods.setValue("metadata.disableStandardEmails.confirmation.host", e);
onChange(e);
}}
/>
</>
)}
/>
</>
<Controller
name="metadata.disableStandardEmails.confirmation.host"
defaultValue={!!formMethods.getValues("seatsPerTimeSlot")}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_host_confirmation_emails")}
description={t("disable_host_confirmation_emails_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
</>
)}
/>
)}
{showEventNameTip && (
<CustomEventTypeModal
close={closeEventNameTip}
setValue={setEventName}
defaultValue={formMethods.getValues("eventName") || eventType.eventName || ""}
setValue={(val: string) => formMethods.setValue("eventName", val)}
defaultValue={formMethods.getValues("eventName")}
placeHolder={eventNamePlaceholder}
event={eventNameObject}
/>
@@ -23,16 +23,16 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
teamId: eventType.team?.id || eventType.parent?.teamId,
});
const methods = useFormContext<FormValues>();
const formMethods = useFormContext<FormValues>();
const installedApps =
eventTypeApps?.items.filter((app) => app.userCredentialIds.length || app.teams.length) || [];
const notInstalledApps =
eventTypeApps?.items.filter((app) => !app.userCredentialIds.length && !app.teams.length) || [];
const allAppsData = methods.watch("metadata")?.apps || {};
const allAppsData = formMethods.watch("metadata")?.apps || {};
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
methods.setValue("metadata", {
...methods.getValues("metadata"),
formMethods.setValue("metadata", {
...formMethods.getValues("metadata"),
apps: _allAppsData,
});
};
@@ -47,7 +47,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
};
};
const eventTypeFormMetadata = methods.getValues("metadata");
const eventTypeFormMetadata = formMethods.getValues("metadata");
const getAppDataSetter = (
appId: EventTypeAppsList,
@@ -56,8 +56,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
): SetAppData => {
return function (key, value) {
// Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
const allAppsDataFromForm = formMethods.getValues("metadata")?.apps || {};
const appData = allAppsDataFromForm[appId];
setAllAppsData({
...allAppsDataFromForm,
@@ -1,10 +1,10 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as RadioGroup from "@radix-ui/react-radio-group";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import type { FormValues } from "pages/event-types/[type]";
import type { Key } from "react";
import React, { useEffect, useState } from "react";
import type { UseFormRegisterReturn } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { Controller, useFormContext } from "react-hook-form";
import type { SingleValue } from "react-select";
import { classNames } from "@calcom/lib";
@@ -107,7 +107,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
);
});
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
export const EventLimitsTab = () => {
const { t, i18n } = useLocale();
const formMethods = useFormContext<FormValues>();
@@ -126,36 +126,20 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
},
];
const periodType =
PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
PERIOD_TYPES.find((s) => s.type === "UNLIMITED");
const [periodDates] = useState<{ startDate: Date; endDate: Date }>({
startDate: new Date(eventType.periodStartDate || Date.now()),
endDate: new Date(eventType.periodEndDate || Date.now()),
});
const watchPeriodType = useWatch({
control: formMethods.control,
name: "periodType",
defaultValue: periodType?.type,
});
const watchPeriodType = formMethods.watch("periodType");
const optionsPeriod = [
{ value: 1, label: t("calendar_days") },
{ value: 0, label: t("business_days") },
{ value: 1, label: t("calendar_days") },
];
// offsetStart toggle is client-side only, opened by default if offsetStart is set
const offsetStartValue = useWatch({
control: formMethods.control,
name: "offsetStart",
});
const [offsetToggle, setOffsetToggle] = useState(() => offsetStartValue > 0);
const [offsetToggle, setOffsetToggle] = useState(formMethods.getValues("offsetStart") > 0);
// Preview how the offset will affect start times
const watchOffsetStartValue = formMethods.watch("offsetStart");
const offsetOriginalTime = new Date();
offsetOriginalTime.setHours(9, 0, 0, 0);
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + offsetStartValue * 60 * 1000);
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + watchOffsetStartValue * 60 * 1000);
return (
<div>
@@ -165,8 +149,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<Label htmlFor="beforeBufferTime">{t("before_event")}</Label>
<Controller
name="beforeBufferTime"
control={formMethods.control}
defaultValue={eventType.beforeEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const beforeBufferOptions = [
{
@@ -197,8 +179,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<Label htmlFor="afterBufferTime">{t("after_event")}</Label>
<Controller
name="afterBufferTime"
control={formMethods.control}
defaultValue={eventType.afterEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const afterBufferOptions = [
{
@@ -235,7 +215,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<Label htmlFor="slotInterval">{t("slot_interval")}</Label>
<Controller
name="slotInterval"
control={formMethods.control}
render={() => {
const slotIntervalOptions = [
{
@@ -254,8 +233,9 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
}}
defaultValue={
slotIntervalOptions.find((option) => option.value === eventType.slotInterval) ||
slotIntervalOptions[0]
slotIntervalOptions.find(
(option) => option.value === formMethods.getValues("slotInterval")
) || slotIntervalOptions[0]
}
options={slotIntervalOptions}
/>
@@ -267,7 +247,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</div>
<Controller
name="bookingLimits"
control={formMethods.control}
render={({ field: { value } }) => {
const isChecked = Object.keys(value ?? {}).length > 0;
return (
@@ -300,7 +279,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
<Controller
name="onlyShowFirstAvailableSlot"
control={formMethods.control}
render={({ field: { value } }) => {
const isChecked = value;
return (
@@ -323,7 +301,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
<Controller
name="durationLimits"
control={formMethods.control}
render={({ field: { value } }) => {
const isChecked = Object.keys(value ?? {}).length > 0;
return (
@@ -361,7 +338,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
<Controller
name="periodType"
control={formMethods.control}
render={({ field: { value } }) => {
const isChecked = value && value !== "UNLIMITED";
@@ -380,7 +356,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
<RadioGroup.Root
defaultValue={watchPeriodType}
value={watchPeriodType}
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
{PERIOD_TYPES.map((period) => {
@@ -408,22 +383,23 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
placeholder="30"
{...formMethods.register("periodDays", { valueAsNumber: true })}
defaultValue={eventType.periodDays || 30}
/>
<Select
options={optionsPeriod}
isSearchable={false}
onChange={(opt) => {
formMethods.setValue(
"periodCountCalendarDays",
opt?.value.toString() as "0" | "1"
);
}}
defaultValue={
optionsPeriod.find(
(opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0)
) ?? optionsPeriod[0]
onChange={(opt) =>
formMethods.setValue("periodCountCalendarDays", opt?.value === 1 ? "1" : "0")
}
name="periodCoundCalendarDays"
value={optionsPeriod.find((opt) => {
opt.value ===
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0);
})}
defaultValue={optionsPeriod.find(
(opt) =>
opt.value ===
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0)
)}
/>
</div>
)}
@@ -431,8 +407,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<div className="me-2 ms-2 inline-flex space-x-2 rtl:space-x-reverse">
<Controller
name="periodDates"
control={formMethods.control}
defaultValue={periodDates}
render={() => (
<DateRangePicker
startDate={formMethods.getValues("periodDates").startDate}
@@ -481,7 +455,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
type="number"
containerClassName="max-w-80"
label={t("offset_start")}
{...formMethods.register("offsetStart")}
{...formMethods.register("offsetStart", { setValueAs: (value) => Number(value) })}
addOnSuffix={<>{t("minutes")}</>}
hint={t("offset_start_description", {
originalTime: offsetOriginalTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
@@ -9,9 +9,5 @@ export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "even
const requirePayment = paymentAppData.price > 0;
return (
<div className="">
<RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />
</div>
);
return <RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />;
};
+27 -42
View File
@@ -48,21 +48,19 @@ const getLocationFromType = (
}
};
const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
const getLocationInfo = ({
eventType,
locationOptions,
}: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
const locationAvailable =
props.eventType.locations &&
props.eventType.locations.length > 0 &&
props.locationOptions.some((op) =>
op.options.find((opt) => opt.value === props.eventType.locations[0].type)
);
const locationDetails = props.eventType.locations &&
props.eventType.locations.length > 0 &&
eventType.locations &&
eventType.locations.length > 0 &&
locationOptions.some((op) => op.options.find((opt) => opt.value === eventType.locations[0].type));
const locationDetails = eventType.locations &&
eventType.locations.length > 0 &&
!locationAvailable && {
slug: props.eventType.locations[0].type
.replace("integrations:", "")
.replace(":", "-")
.replace("_video", ""),
name: props.eventType.locations[0].type
slug: eventType.locations[0].type.replace("integrations:", "").replace(":", "-").replace("_video", ""),
name: eventType.locations[0].type
.replace("integrations:", "")
.replace(":", " ")
.replace("_video", "")
@@ -72,16 +70,11 @@ const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locatio
};
return { locationAvailable, locationDetails };
};
interface DescriptionEditorProps {
description?: string | null;
editable?: boolean;
}
const DescriptionEditor = (props: DescriptionEditorProps) => {
const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
const formMethods = useFormContext<FormValues>();
const [mounted, setIsMounted] = useState(false);
const { t } = useLocale();
const { description } = props;
const [firstRender, setFirstRender] = useState(true);
useEffect(() => {
setIsMounted(true);
@@ -89,11 +82,11 @@ const DescriptionEditor = (props: DescriptionEditorProps) => {
return mounted ? (
<Editor
getText={() => md.render(formMethods.getValues("description") || description || "")}
getText={() => md.render(formMethods.getValues("description") || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value))}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
editable={props.editable}
editable={isEditable}
firstRender={firstRender}
setFirstRender={setFirstRender}
/>
@@ -113,7 +106,9 @@ export const EventSetupTab = (
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const { eventType, team, destinationCalendar } = props;
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration);
const [multipleDuration, setMultipleDuration] = useState(
formMethods.getValues("metadata")?.multipleDuration
);
const orgBranding = useOrgBranding();
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
@@ -143,12 +138,12 @@ export const EventSetupTab = (
}>
>(multipleDurationOptions.filter((mdOpt) => multipleDuration?.includes(mdOpt.value)));
const [defaultDuration, setDefaultDuration] = useState(
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
selectedMultipleDuration.find((opt) => opt.value === formMethods.getValues("length")) ?? null
);
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
useLockedFieldsManager(
eventType,
formMethods.getValues(),
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
@@ -195,7 +190,6 @@ export const EventSetupTab = (
return (
<Controller
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
control={formMethods.control}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
@@ -219,7 +213,6 @@ export const EventSetupTab = (
return (
<Controller
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
control={formMethods.control}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
@@ -339,7 +332,7 @@ export const EventSetupTab = (
defaultChecked={defaultLocation?.displayLocationPublicly}
description={t("display_location_label")}
onChange={(e) => {
const fieldValues = formMethods.getValues().locations[index];
const fieldValues = formMethods.getValues("locations")[index];
updateLocationField(index, {
...fieldValues,
displayLocationPublicly: e.target.checked,
@@ -408,7 +401,7 @@ export const EventSetupTab = (
The Add to calendar for this event type needs to be a Google Calendar for Meet to work.
Change it{" "}
<Link
href={`${CAL_URL}/event-types/${eventType.id}?tabName=advanced`}
href={`${CAL_URL}/event-types/${formMethods.getValues("id")}?tabName=advanced`}
className="underline">
here.
</Link>{" "}
@@ -463,7 +456,6 @@ export const EventSetupTab = (
required
label={t("title")}
{...shouldLockDisableProps("title")}
defaultValue={eventType.title}
{...formMethods.register("title")}
/>
<div>
@@ -471,23 +463,19 @@ export const EventSetupTab = (
{t("description")}
{shouldLockIndicator("description")}
</Label>
<DescriptionEditor
description={eventType?.description}
editable={!descriptionLockedProps.disabled}
/>
<DescriptionEditor isEditable={!descriptionLockedProps.disabled} />
</div>
<TextField
required
label={t("URL")}
{...shouldLockDisableProps("slug")}
defaultValue={eventType.slug}
addOnLeading={
<>
{urlPrefix}/
{!isManagedEventType
? team
? (orgBranding ? "" : "team/") + team.slug
: eventType.users[0].username
: formMethods.getValues("users")[0].username
: t("username_placeholder")}
/
</>
@@ -564,7 +552,7 @@ export const EventSetupTab = (
type="number"
{...lengthLockedProps}
label={t("duration")}
defaultValue={eventType.length ?? 15}
defaultValue={formMethods.getValues("length") ?? 15}
{...formMethods.register("length")}
addOnSuffix={<>{t("minutes")}</>}
min={1}
@@ -580,6 +568,8 @@ export const EventSetupTab = (
onCheckedChange={() => {
if (multipleDuration !== undefined) {
setMultipleDuration(undefined);
setSelectedMultipleDuration([]);
setDefaultDuration(null);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
} else {
@@ -600,12 +590,7 @@ export const EventSetupTab = (
{shouldLockIndicator("locations")}
</Skeleton>
<Controller
name="locations"
control={formMethods.control}
defaultValue={eventType.locations || []}
render={() => <Locations />}
/>
<Controller name="locations" render={() => <Locations />} />
</div>
</div>
</div>
@@ -67,55 +67,66 @@ type Props = {
availability?: AvailabilityOption;
isUserOrganizationAdmin: boolean;
bookerUrl: string;
activeWebhooksNumber: number;
};
function getNavigation(props: {
type getNavigationProps = {
t: TFunction;
eventType: Props["eventType"];
length: number;
id: number;
multipleDuration?: EventTypeSetupProps["eventType"]["metadata"]["multipleDuration"];
enabledAppsNumber: number;
enabledWorkflowsNumber: number;
installedAppsNumber: number;
availability: AvailabilityOption | undefined;
}) {
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
const duration =
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
};
function getNavigation({
length,
id,
multipleDuration,
t,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
}: getNavigationProps) {
const duration = multipleDuration?.map((duration) => ` ${duration}`) || length;
return [
{
name: "event_setup_tab_title",
href: `/event-types/${eventType.id}?tabName=setup`,
href: `/event-types/${id}?tabName=setup`,
icon: LinkIcon,
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
},
{
name: "event_limit_tab_title",
href: `/event-types/${eventType.id}?tabName=limits`,
href: `/event-types/${id}?tabName=limits`,
icon: Clock,
info: `event_limit_tab_description`,
},
{
name: "event_advanced_tab_title",
href: `/event-types/${eventType.id}?tabName=advanced`,
href: `/event-types/${id}?tabName=advanced`,
icon: Sliders,
info: `event_advanced_tab_description`,
},
{
name: "recurring",
href: `/event-types/${eventType.id}?tabName=recurring`,
href: `/event-types/${id}?tabName=recurring`,
icon: Repeat,
info: `recurring_event_tab_description`,
},
{
name: "apps",
href: `/event-types/${eventType.id}?tabName=apps`,
href: `/event-types/${id}?tabName=apps`,
icon: Grid,
//TODO: Handle proper translation with count handling
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
},
{
name: "workflows",
href: `/event-types/${eventType.id}?tabName=workflows`,
href: `/event-types/${id}?tabName=workflows`,
icon: Zap,
info: `${enabledWorkflowsNumber} ${t("active")}`,
},
@@ -136,6 +147,7 @@ function EventTypeSingleLayout({
availability,
isUserOrganizationAdmin,
bookerUrl,
activeWebhooksNumber,
}: Props) {
const utils = trpc.useContext();
const { t } = useLocale();
@@ -145,7 +157,7 @@ function EventTypeSingleLayout({
const hasPermsToDelete =
currentUserMembership?.role !== "MEMBER" ||
!currentUserMembership ||
eventType.schedulingType === SchedulingType.MANAGED ||
formMethods.getValues("schedulingType") === SchedulingType.MANAGED ||
isUserOrganizationAdmin;
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
@@ -167,16 +179,23 @@ function EventTypeSingleLayout({
});
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
formMethods.getValues(),
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const length = formMethods.watch("length");
const multipleDuration = formMethods.watch("metadata")?.multipleDuration;
const watchSchedulingType = formMethods.watch("schedulingType");
const watchChildrenCount = formMethods.watch("children").length;
// Define tab navigation here
const EventTypeTabs = useMemo(() => {
const navigation = getNavigation({
t,
eventType,
length,
multipleDuration,
id: formMethods.getValues("id"),
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
@@ -185,31 +204,29 @@ function EventTypeSingleLayout({
navigation.splice(1, 0, {
name: "availability",
href: `/event-types/${eventType.id}?tabName=availability`,
href: `/event-types/${formMethods.getValues("id")}?tabName=availability`,
icon: Calendar,
info:
isManagedEventType || isChildrenManagedEventType
? eventType.schedule === null
? formMethods.getValues("schedule") === null
? "members_default_schedule"
: isChildrenManagedEventType
? `${
eventType.scheduleName
? `${eventType.scheduleName} - ${t("managed")}`
formMethods.getValues("scheduleName")
? `${formMethods.getValues("scheduleName")} - ${t("managed")}`
: `default_schedule_name`
}`
: eventType.scheduleName ?? `default_schedule_name`
: eventType.scheduleName ?? `default_schedule_name`,
: formMethods.getValues("scheduleName") ?? `default_schedule_name`
: formMethods.getValues("scheduleName") ?? `default_schedule_name`,
});
// If there is a team put this navigation item within the tabs
if (team) {
navigation.splice(2, 0, {
name: "assignment",
href: `/event-types/${eventType.id}?tabName=team`,
href: `/event-types/${formMethods.getValues("id")}?tabName=team`,
icon: Users,
info: `${t(eventType.schedulingType?.toLowerCase() ?? "")}${
isManagedEventType
? ` - ${t("number_member", { count: formMethods.watch("children").length || 0 })}`
: ""
info: `${t(watchSchedulingType?.toLowerCase() ?? "")}${
isManagedEventType ? ` - ${t("number_member", { count: watchChildrenCount || 0 })}` : ""
}`,
});
}
@@ -225,15 +242,14 @@ function EventTypeSingleLayout({
}
navigation.push({
name: "webhooks",
href: `/event-types/${eventType.id}?tabName=webhooks`,
href: `/event-types/${formMethods.getValues("id")}?tabName=webhooks`,
icon: TbWebhook,
info: `${eventType.webhooks.filter((webhook) => webhook.active).length} ${t("active")}`,
info: `${activeWebhooksNumber} ${t("active")}`,
});
}
return navigation;
}, [
t,
eventType,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
@@ -241,16 +257,23 @@ function EventTypeSingleLayout({
isManagedEventType,
isChildrenManagedEventType,
team,
formMethods,
length,
multipleDuration,
formMethods.getValues("id"),
watchSchedulingType,
watchChildrenCount,
activeWebhooksNumber,
]);
const permalink = `${bookerUrl}/${
team ? `${!team.parentId ? "team/" : ""}${team.slug}` : eventType.users[0].username
team ? `${!team.parentId ? "team/" : ""}${team.slug}` : formMethods.getValues("users")[0].username
}/${eventType.slug}`;
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
const isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
const embedLink = `${
team ? `team/${team.slug}` : formMethods.getValues("users")[0].username
}/${formMethods.getValues("slug")}`;
const isManagedEvent = formMethods.getValues("schedulingType") === SchedulingType.MANAGED ? "_managed" : "";
// const title = formMethods.watch("title");
return (
<Shell
backPath="/event-types"
@@ -258,7 +281,7 @@ function EventTypeSingleLayout({
heading={eventType.title}
CTA={
<div className="flex items-center justify-end">
{!eventType.metadata?.managedEventConfig && (
{!formMethods.getValues("metadata")?.managedEventConfig && (
<>
<div
className={classNames(
@@ -333,7 +356,7 @@ function EventTypeSingleLayout({
tooltip={t("embed")}
tooltipSide="bottom"
tooltipOffset={4}
eventId={eventType.id}
eventId={formMethods.getValues("id")}
/>
</>
)}
@@ -453,7 +476,7 @@ function EventTypeSingleLayout({
loadingText={t(`confirm_delete_event_type`)}
onConfirm={(e) => {
e.preventDefault();
deleteMutation.mutate({ id: eventType.id });
deleteMutation.mutate({ id: formMethods.getValues("id") });
}}>
<p className="mt-5">
<Trans
@@ -23,7 +23,7 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({ eventTypeId: eventType.id });
const { data: installedApps, isPending } = trpc.viewer.integrations.useQuery({
const { data: installedApps, isLoading } = trpc.viewer.integrations.useQuery({
variant: "other",
onlyInstalled: true,
});
@@ -35,8 +35,9 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
async onSuccess() {
setEditModalOpen(false);
await utils.viewer.webhook.list.invalidate();
showToast(t("webhook_updated_successfully"), "success");
await utils.viewer.webhook.list.invalidate();
await utils.viewer.eventTypes.get.invalidate();
},
onError(error) {
showToast(`${error.message}`, "error");
@@ -45,9 +46,10 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
async onSuccess() {
setCreateModalOpen(false);
showToast(t("webhook_created_successfully"), "success");
await utils.viewer.webhook.list.invalidate();
setCreateModalOpen(false);
await utils.viewer.eventTypes.get.invalidate();
},
onError(error) {
showToast(`${error.message}`, "error");
@@ -103,7 +105,7 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
return (
<div>
{webhooks && !isPending && (
{webhooks && !isLoading && (
<>
<div>
<div>
@@ -19,11 +19,10 @@ export default function RecurringEventController({
paymentEnabled,
}: RecurringEventControllerProps) {
const { t } = useLocale();
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
eventType.recurringEvent
);
const formMethods = useFormContext<FormValues>();
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
formMethods.getValues("recurringEvent")
);
/* Just yearly-0, monthly-1 and weekly-2 */
const recurringEventFreqOptions = Object.entries(Frequency)
.filter(([key, value]) => isNaN(Number(key)) && Number(value) < 3)
+23 -4
View File
@@ -80,6 +80,7 @@ const EventWebhooksTab = dynamic(() =>
const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/ManagedEventDialog"));
export type FormValues = {
id: number;
title: string;
eventTitle: string;
eventName: string;
@@ -119,6 +120,7 @@ export type FormValues = {
seatsShowAttendees: boolean | null;
seatsShowAvailabilityCount: boolean | null;
seatsPerTimeSlotEnabled: boolean;
scheduleName: string;
minimumBookingNotice: number;
minimumBookingNoticeInDurationType: number;
beforeBufferTime: number;
@@ -139,6 +141,7 @@ export type FormValues = {
availability?: AvailabilityOption;
bookerLayouts: BookerLayoutSettings;
multipleDurationEnabled: boolean;
users: EventTypeSetup["users"];
};
export type CustomInputParsed = typeof customInputSchema._output;
@@ -180,7 +183,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
onSuccess: async () => {
formMethods.setValue(
@@ -248,6 +250,18 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const defaultValues: any = useMemo(() => {
return {
title: eventType.title,
id: eventType.id,
slug: eventType.slug,
afterEventBuffer: eventType.afterEventBuffer,
beforeEventBuffer: eventType.beforeEventBuffer,
eventName: eventType.eventName || "",
scheduleName: eventType.scheduleName,
periodDays: eventType.periodDays || 30,
requiresBookerEmailVerification: eventType.requiresBookerEmailVerification,
seatsPerTimeSlot: eventType.seatsPerTimeSlot,
seatsShowAttendees: eventType.seatsShowAttendees,
seatsShowAvailabilityCount: eventType.seatsShowAvailabilityCount,
lockTimeZoneToggleOnBookingPage: eventType.lockTimeZoneToggleOnBookingPage,
locations: eventType.locations || [],
recurringEvent: eventType.recurringEvent || null,
isInstantEvent: eventType.isInstantEvent,
@@ -268,9 +282,13 @@ const EventTypePage = (props: EventTypeSetupProps) => {
periodType: eventType.periodType,
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
schedulingType: eventType.schedulingType,
requiresConfirmation: eventType.requiresConfirmation,
slotInterval: eventType.slotInterval,
minimumBookingNotice: eventType.minimumBookingNotice,
metadata,
hosts: eventType.hosts,
successRedirectUrl: eventType.successRedirectUrl || "",
users: eventType.users,
children: eventType.children.map((ch) => ({
...ch,
created: true,
@@ -411,7 +429,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
),
availability: <EventAvailabilityTab eventType={eventType} isTeamEvent={!!team} />,
team: <EventTeamTab teamMembers={teamMembers} team={team} eventType={eventType} />,
limits: <EventLimitsTab eventType={eventType} />,
limits: <EventLimitsTab />,
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
instant: <EventInstantTab eventType={eventType} isTeamEvent={!!team} />,
recurring: <EventRecurringTab eventType={eventType} />,
@@ -490,7 +508,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, ...rest } = input;
const { availability, users, scheduleName, ...rest } = input;
updateMutation.mutate({
...rest,
length,
@@ -550,6 +568,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
installedAppsNumber={eventTypeApps?.items.length || 0}
enabledWorkflowsNumber={eventType.workflows.length}
eventType={eventType}
activeWebhooksNumber={eventType.webhooks.filter((webhook) => webhook.active).length}
team={team}
availability={availability}
isUpdateMutationLoading={updateMutation.isPending}
@@ -618,7 +637,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
throw new Error(t("event_setup_multiple_payment_apps_error"));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, ...rest } = input;
const { availability, users, scheduleName, ...rest } = input;
updateMutation.mutate({
...rest,
length,
@@ -54,7 +54,6 @@ const DestinationCalendarSelector = ({
hidePlaceholder,
hideAdvancedText,
maxWidth,
destinationCalendar,
}: Props): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.connectedCalendars.useQuery();
@@ -42,17 +42,19 @@ export default function WebhookListItem(props: {
const deleteWebhook = trpc.viewer.webhook.delete.useMutation({
async onSuccess() {
showToast(t("webhook_removed_successfully"), "success");
await utils.viewer.webhook.getByViewer.invalidate();
await utils.viewer.webhook.list.invalidate();
showToast(t("webhook_removed_successfully"), "success");
await utils.viewer.eventTypes.get.invalidate();
},
});
const toggleWebhook = trpc.viewer.webhook.edit.useMutation({
async onSuccess(data) {
await utils.viewer.webhook.getByViewer.invalidate();
await utils.viewer.webhook.list.invalidate();
// TODO: Better success message
showToast(t(data?.active ? "enabled" : "disabled"), "success");
await utils.viewer.webhook.getByViewer.invalidate();
await utils.viewer.webhook.list.invalidate();
await utils.viewer.eventTypes.get.invalidate();
},
});