From ef45cbfb3ff743959a5fa58dc7b0ca5b69fa8478 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Thu, 28 Sep 2023 17:29:06 +0530 Subject: [PATCH] refactor: event type settings (#11539) Co-authored-by: Peer Richelsen Co-authored-by: Peer Richelsen --- .../components/eventtype/EventAdvancedTab.tsx | 260 +++++++------ .../web/components/eventtype/EventAppsTab.tsx | 4 +- .../eventtype/EventAvailabilityTab.tsx | 77 ++-- .../components/eventtype/EventLimitsTab.tsx | 361 ++++++++++-------- .../components/eventtype/EventSetupTab.tsx | 335 ++++++++-------- .../components/eventtype/EventWebhooksTab.tsx | 52 ++- .../eventtype/RecurringEventController.tsx | 131 ++++--- .../RequiresConfirmationController.tsx | 198 +++++----- apps/web/pages/event-types/[type]/index.tsx | 3 +- apps/web/public/static/locales/en/common.json | 2 +- .../calendars/DestinationCalendarSelector.tsx | 6 +- .../features/form-builder/FormBuilder.tsx | 8 +- .../webhooks/components/WebhookListItem.tsx | 4 +- packages/ui/components/form/select/Select.tsx | 33 +- .../components/form/switch/SettingsToggle.tsx | 73 +++- 15 files changed, 861 insertions(+), 686 deletions(-) diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx index c95d28a4f9..9379776d51 100644 --- a/apps/web/components/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/eventtype/EventAdvancedTab.tsx @@ -34,7 +34,7 @@ import { TextField, Tooltip, } from "@calcom/ui"; -import { Copy, Edit } from "@calcom/ui/components/icon"; +import { Copy, Edit, Info } from "@calcom/ui/components/icon"; import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants"; import RequiresConfirmationController from "./RequiresConfirmationController"; @@ -124,79 +124,81 @@ export const EventAdvancedTab = ({ eventType, team }: Pick formMethods.setValue("eventName", value); return ( -
+
{/** * Only display calendar selector if user has connected calendars AND if it's not * a team event. Since we don't have logic to handle each attendee calendar (for now). * This will fallback to each user selected destination calendar. */} - {!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && ( -
-
- - - {t("add_another_calendar")} - +
+ {!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && ( +
+
+ + + {t("add_another_calendar")} + +
+
+ ( + + )} + /> +
+

{t("select_which_cal")}

-
- ( - - )} - /> -
-

{t("select_which_cal")}

+ )} +
+ setShowEventNameTip((old) => !old)}> + + + } + />
- )} -
- setShowEventNameTip((old) => !old)}> - - - } +
+ + + +
+
-
-
- -
-
- -
+ -
+ ( )} /> -
+ ( )} /> -
+ ( <> - {/* Textfield has some margin by default we remove that so we can keep consistent alignment */} -
+
)} /> -
+ + + + } {...shouldLockDisableProps("hashedLinkCheck")} description={t("private_link_description", { appName: APP_NAME })} checked={hashedLinkVisible} @@ -285,8 +310,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick - {/* Textfield has some margin by default we remove that so we can keep consitant aligment */} -
+
{!IS_VISUAL_REGRESSION_TESTING && ( -
+ ( <> - ( -
- {t("seats")}} - onChange={(e) => { - onChange(Math.abs(Number(e.target.value))); - }} - /> -
- + ( +
+ formMethods.setValue("seatsShowAttendees", e.target.checked)} - defaultChecked={!!eventType.seatsShowAttendees} + defaultValue={value || 2} + min={1} + addOnSuffix={<>{t("seats")}} + onChange={(e) => { + onChange(Math.abs(Number(e.target.value))); + }} /> +
+ formMethods.setValue("seatsShowAttendees", e.target.checked)} + defaultChecked={!!eventType.seatsShowAttendees} + /> +
+
+ + formMethods.setValue("seatsShowAvailabilityCount", e.target.checked) + } + defaultChecked={!!eventType.seatsShowAvailabilityCount} + /> +
-
- formMethods.setValue("seatsShowAvailabilityCount", e.target.checked)} - defaultChecked={!!eventType.seatsShowAvailabilityCount} - /> -
-
- )} - /> + )} + /> +
{noShowFeeEnabled && } @@ -395,13 +429,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick {allowDisablingAttendeeConfirmationEmails(workflows) && ( <> -
( <> -
( <> {
{!shouldLockDisableProps("apps").disabled && ( -
+
{!isLoading && notInstalledApps?.length ? ( <>

@@ -166,7 +166,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {

- You have no apps installed. View popular apps below and explore more in our   + View popular apps below and explore more in our   App Store diff --git a/apps/web/components/eventtype/EventAvailabilityTab.tsx b/apps/web/components/eventtype/EventAvailabilityTab.tsx index 07ed8a9515..caf04147de 100644 --- a/apps/web/components/eventtype/EventAvailabilityTab.tsx +++ b/apps/web/components/eventtype/EventAvailabilityTab.tsx @@ -98,42 +98,43 @@ const EventTypeScheduleDetails = memo( schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || []; return ( -

-
    - {weekdayNames(i18n.language, 1, "long").map((day, index) => { - const isAvailable = !!filterDays(index).length; - return ( -
  1. - - {day} - - {isLoading ? ( - - ) : isAvailable ? ( -
    - {filterDays(index).map((dayRange, i) => ( -
    - - {format(dayRange.startTime, timeFormat === 12)} - - - -
    {format(dayRange.endTime, timeFormat === 12)}
    -
    - ))} -
    - ) : ( - {t("unavailable")} - )} -
  2. - ); - })} -
-
-
+
+
+
    + {weekdayNames(i18n.language, 1, "long").map((day, index) => { + const isAvailable = !!filterDays(index).length; + return ( +
  1. + + {day} + + {isLoading ? ( + + ) : isAvailable ? ( +
    + {filterDays(index).map((dayRange, i) => ( +
    + + {format(dayRange.startTime, timeFormat === 12)} + + - +
    {format(dayRange.endTime, timeFormat === 12)}
    +
    + ))} +
    + ) : ( + {t("unavailable")} + )} +
  2. + ); + })} +
+
+
{schedule?.timeZone || } @@ -234,8 +235,8 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => { }, [availabilityValue, setValue]); return ( -
-
+
+
{ - formMethods.setValue( - "periodCountCalendarDays", - opt?.value.toString() as "0" | "1" - ); - }} - defaultValue={ - optionsPeriod.find( - (opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0) - ) ?? optionsPeriod[0] - } - /> -
- )} - {period.type === "RANGE" && ( -
- ( - { + const isChecked = value && value !== "UNLIMITED"; + + return ( + formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}> +
+ formMethods.setValue("periodType", val as PeriodType)}> + {PERIOD_TYPES.filter((opt) => + periodTypeLocked.disabled ? watchPeriodType === opt.type : true + ).map((period) => { + if (period.type === "UNLIMITED") return null; + return ( +
+ {!periodTypeLocked.disabled && ( + + + + )} + {period.prefix ? {period.prefix}  : null} + {period.type === "ROLLING" && ( +
+ { - formMethods.setValue("periodDates", { - startDate, - endDate, - }); - }} + {...formMethods.register("periodDays", { valueAsNumber: true })} + defaultValue={eventType.periodDays || 30} /> - )} - /> + option.value === limitKey)} onChange={onIntervalSelect} + className="w-36" /> {hasDeleteButton && !disabled && ( -
); diff --git a/apps/web/components/eventtype/EventSetupTab.tsx b/apps/web/components/eventtype/EventSetupTab.tsx index 07a7d7d3b3..e210cf4eaf 100644 --- a/apps/web/components/eventtype/EventSetupTab.tsx +++ b/apps/web/components/eventtype/EventSetupTab.tsx @@ -387,178 +387,185 @@ export const EventSetupTab = ( return (
-
- -
- - -
- - {urlPrefix}/ - {!isManagedEventType - ? team - ? (orgBranding ? "" : "team/") + team.slug - : eventType.users[0].username - : t("username_placeholder")} - / - - } - {...formMethods.register("slug", { - setValueAs: (v) => slugify(v), - })} - /> - {multipleDuration ? ( -
-
- - {t("available_durations")} - - t("default_duration_no_options")} - options={selectedMultipleDuration} - onChange={(option) => { - setDefaultDuration( - selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null - ); - if (option) formMethods.setValue("length", option.value); - }} - /> -
-
- ) : ( +
+
{t("minutes")}} - min={1} + label={t("title")} + {...shouldLockDisableProps("title")} + defaultValue={eventType.title} + {...formMethods.register("title")} /> - )} - {!lengthLockedProps.disabled && ( -
- { - if (multipleDuration !== undefined) { - setMultipleDuration(undefined); - formMethods.setValue("metadata.multipleDuration", undefined); - formMethods.setValue("length", eventType.length); - } else { - setMultipleDuration([]); - formMethods.setValue("metadata.multipleDuration", []); - formMethods.setValue("length", 0); - } - }} +
+ +
- )} -
- - {t("location")} - {shouldLockIndicator("locations")} - - - } + + {urlPrefix}/ + {!isManagedEventType + ? team + ? (orgBranding ? "" : "team/") + team.slug + : eventType.users[0].username + : t("username_placeholder")} + / + + } + {...formMethods.register("slug", { + setValueAs: (v) => slugify(v), + })} />
-
+
+ {multipleDuration ? ( +
+
+ + {t("available_durations")} + + t("default_duration_no_options")} + options={selectedMultipleDuration} + onChange={(option) => { + setDefaultDuration( + selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null + ); + if (option) formMethods.setValue("length", option.value); + }} + /> +
+
+ ) : ( + {t("minutes")}} + min={1} + /> + )} + {!lengthLockedProps.disabled && ( +
+ { + if (multipleDuration !== undefined) { + setMultipleDuration(undefined); + formMethods.setValue("metadata.multipleDuration", undefined); + formMethods.setValue("length", eventType.length); + } else { + setMultipleDuration([]); + formMethods.setValue("metadata.multipleDuration", []); + formMethods.setValue("length", 0); + } + }} + /> +
+ )} +
- {/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */} - +
+
+ + {t("location")} + {shouldLockIndicator("locations")} + + + } + /> +
+
+ + {/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */} + +
); }; diff --git a/apps/web/components/eventtype/EventWebhooksTab.tsx b/apps/web/components/eventtype/EventWebhooksTab.tsx index 59f7bc7d36..b00bc86362 100644 --- a/apps/web/components/eventtype/EventWebhooksTab.tsx +++ b/apps/web/components/eventtype/EventWebhooksTab.tsx @@ -1,5 +1,7 @@ import type { Webhook } from "@prisma/client"; import { Webhook as TbWebhook } from "lucide-react"; +import { Trans } from "next-i18next"; +import Link from "next/link"; import type { EventTypeSetupProps } from "pages/event-types/[type]"; import { useState } from "react"; @@ -8,6 +10,7 @@ import { WebhookForm } from "@calcom/features/webhooks/components"; import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm"; import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem"; import { subscriberUrlReserved } from "@calcom/features/webhooks/lib/subscriberUrlReserved"; +import { APP_NAME } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Alert, Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui"; @@ -115,23 +118,40 @@ export const EventWebhooksTab = ({ eventType }: Pick -
- {webhooks.map((webhook, index) => { - return ( - { - setEditModalOpen(true); - setWebhookToEdit(webhook); - }} - /> - ); - })} +
+
{t("webhooks")}
+

+ {t("add_webhook_description", { appName: APP_NAME })} +

+ +
+ {webhooks.map((webhook, index) => { + return ( + { + setEditModalOpen(true); + setWebhookToEdit(webhook); + }} + /> + ); + })} +
+ +

+ + If you wish to edit or manage your web hooks, please head over to   + + webhooks settings + + +

- ) : ( - {recurringEventState && ( -
-
-

{t("repeats_every")}

- { - const newVal = { - ...recurringEventState, - interval: parseInt(event?.target.value), - }; - formMethods.setValue("recurringEvent", newVal); - setRecurringEventState(newVal); - }} - /> - { + const newVal = { + ...recurringEventState, + freq: parseInt(event?.value || `${Frequency.WEEKLY}`), + }; + formMethods.setValue("recurringEvent", newVal); + setRecurringEventState(newVal); + }} + /> +
+
+

{t("for_a_maximum_of")}

+ { + const newVal = { + ...recurringEventState, + count: parseInt(event?.target.value), + }; + formMethods.setValue("recurringEvent", newVal); + setRecurringEventState(newVal); + }} + /> +

+ {t("events", { + count: recurringEventState.count, + })} +

+
-
-

{t("for_a_maximum_of")}

- { - const newVal = { - ...recurringEventState, - count: parseInt(event?.target.value), - }; - formMethods.setValue("recurringEvent", newVal); - setRecurringEventState(newVal); - }} - /> -

- {t("events", { - count: recurringEventState.count, - })} -

-
-
- )} + )} +
)} diff --git a/apps/web/components/eventtype/RequiresConfirmationController.tsx b/apps/web/components/eventtype/RequiresConfirmationController.tsx index a9f94a28c0..e8789a2a48 100644 --- a/apps/web/components/eventtype/RequiresConfirmationController.tsx +++ b/apps/web/components/eventtype/RequiresConfirmationController.tsx @@ -67,6 +67,12 @@ export default function RequiresConfirmationController({ control={formMethods.control} render={() => ( - { - if (val === "always") { - formMethods.setValue("requiresConfirmation", true); - onRequiresConfirmation(true); - formMethods.setValue("metadata.requiresConfirmationThreshold", undefined); - setRequiresConfirmationSetup(undefined); - } else if (val === "notice") { - formMethods.setValue("requiresConfirmation", true); - onRequiresConfirmation(true); - formMethods.setValue( - "metadata.requiresConfirmationThreshold", - requiresConfirmationSetup || defaultRequiresConfirmationSetup - ); +
+ -
- {(requiresConfirmationSetup === undefined || !requiresConfirmationLockedProps.disabled) && ( - - )} - {(requiresConfirmationSetup !== undefined || !requiresConfirmationLockedProps.disabled) && ( - - - { - const val = Number(evt.target?.value); - setRequiresConfirmationSetup({ - unit: - requiresConfirmationSetup?.unit ?? - defaultRequiresConfirmationSetup.unit, - time: val, - }); - formMethods.setValue( - "metadata.requiresConfirmationThreshold.time", - val - ); - }} - className="border-default !m-0 block w-16 rounded-md text-sm [appearance:textfield]" - defaultValue={metadata?.requiresConfirmationThreshold?.time || 30} - /> - -
- ), - }} - /> - - } - id="notice" - value="notice" - /> - )} -
-
+