From 7ab49acebec834b9811b59f3571039a8ed47f1a9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Omar=20L=C3=B3pez?= <zomars@me.com>
Date: Sun, 26 Sep 2021 15:49:16 -0600
Subject: [PATCH] Fixes eventype form (#777)

* Type fixes

* Uses all integrations and session fixes on getting started page

* eventtype form fixes

* Update pages/event-types/[type].tsx

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
---
 lib/asStringOrNull.tsx                        |  12 ++
 .../event-types/update-event-type.ts          |   6 +-
 lib/types/event-type.ts                       |  10 ++
 pages/api/availability/eventtype.ts           |   4 +-
 pages/event-types/[type].tsx                  | 121 ++++++++----------
 pages/getting-started.tsx                     |  38 +-----
 6 files changed, 88 insertions(+), 103 deletions(-)

diff --git a/lib/asStringOrNull.tsx b/lib/asStringOrNull.tsx
index ce9c8b72..0f41a8fd 100644
--- a/lib/asStringOrNull.tsx
+++ b/lib/asStringOrNull.tsx
@@ -2,6 +2,18 @@ export function asStringOrNull(str: unknown) {
   return typeof str === "string" ? str : null;
 }
 
+export function asStringOrUndefined(str: unknown) {
+  return typeof str === "string" ? str : undefined;
+}
+
+export function asNumberOrUndefined(str: unknown) {
+  return typeof str === "string" ? parseInt(str) : undefined;
+}
+
+export function asNumberOrThrow(str: unknown) {
+  return parseInt(asStringOrThrow(str));
+}
+
 export function asStringOrThrow(str: unknown): string {
   const type = typeof str;
   if (type !== "string") {
diff --git a/lib/mutations/event-types/update-event-type.ts b/lib/mutations/event-types/update-event-type.ts
index 97b58448..0f06280a 100644
--- a/lib/mutations/event-types/update-event-type.ts
+++ b/lib/mutations/event-types/update-event-type.ts
@@ -3,8 +3,12 @@ import { EventType } from "@prisma/client";
 import * as fetch from "@lib/core/http/fetch-wrapper";
 import { EventTypeInput } from "@lib/types/event-type";
 
+type EventTypeResponse = {
+  eventType: EventType;
+};
+
 const updateEventType = async (data: EventTypeInput) => {
-  const response = await fetch.patch<EventTypeInput, EventType>("/api/availability/eventtype", data);
+  const response = await fetch.patch<EventTypeInput, EventTypeResponse>("/api/availability/eventtype", data);
   return response;
 };
 
diff --git a/lib/types/event-type.ts b/lib/types/event-type.ts
index 07c591c3..900d924a 100644
--- a/lib/types/event-type.ts
+++ b/lib/types/event-type.ts
@@ -20,6 +20,16 @@ export type AdvancedOptions = {
   periodEndDate?: Date | string;
   periodCountCalendarDays?: boolean;
   requiresConfirmation?: boolean;
+  disableGuests?: boolean;
+  minimumBookingNotice?: number;
+  price?: number;
+  currency?: string;
+  schedulingType?: SchedulingType;
+  users?: {
+    value: number;
+    label: string;
+    avatar: string;
+  }[];
 };
 
 export type EventTypeCustomInput = {
diff --git a/pages/api/availability/eventtype.ts b/pages/api/availability/eventtype.ts
index ae44188a..e5966b4f 100644
--- a/pages/api/availability/eventtype.ts
+++ b/pages/api/availability/eventtype.ts
@@ -89,7 +89,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
       periodStartDate: req.body.periodStartDate,
       periodEndDate: req.body.periodEndDate,
       periodCountCalendarDays: req.body.periodCountCalendarDays,
-      minimumBookingNotice: req.body.minimumBookingNotice,
+      minimumBookingNotice: req.body.minimumBookingNotice
+        ? parseInt(req.body.minimumBookingNotice)
+        : undefined,
       price: req.body.price,
       currency: req.body.currency,
     };
diff --git a/pages/event-types/[type].tsx b/pages/event-types/[type].tsx
index 5060c145..b53a96bf 100644
--- a/pages/event-types/[type].tsx
+++ b/pages/event-types/[type].tsx
@@ -2,18 +2,18 @@
 import { Disclosure, RadioGroup } from "@headlessui/react";
 import { PhoneIcon, XIcon } from "@heroicons/react/outline";
 import {
-  LocationMarkerIcon,
-  LinkIcon,
-  PlusIcon,
-  DocumentIcon,
   ChevronRightIcon,
   ClockIcon,
-  TrashIcon,
+  DocumentIcon,
   ExternalLinkIcon,
-  UsersIcon,
+  LinkIcon,
+  LocationMarkerIcon,
+  PlusIcon,
+  TrashIcon,
   UserAddIcon,
+  UsersIcon,
 } from "@heroicons/react/solid";
-import { EventTypeCustomInput, EventTypeCustomInputType, SchedulingType } from "@prisma/client";
+import { EventTypeCustomInput, EventTypeCustomInputType, Prisma, SchedulingType } from "@prisma/client";
 import dayjs from "dayjs";
 import timezone from "dayjs/plugin/timezone";
 import utc from "dayjs/plugin/utc";
@@ -28,9 +28,10 @@ import "react-dates/lib/css/_datepicker.css";
 import { FormattedNumber, IntlProvider } from "react-intl";
 import { useMutation } from "react-query";
 import Select, { OptionTypeBase } from "react-select";
-import Stripe from "stripe";
 
-import { asStringOrThrow } from "@lib/asStringOrNull";
+import { StripeData } from "@ee/lib/stripe/server";
+
+import { asNumberOrThrow, asNumberOrUndefined, asStringOrThrow } from "@lib/asStringOrNull";
 import { getSession } from "@lib/auth";
 import classNames from "@lib/classNames";
 import { HttpError } from "@lib/core/http/error";
@@ -144,7 +145,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
     }
   }, [contentSize]);
 
-  const [users, setUsers] = useState([]);
+  const [users, setUsers] = useState<AdvancedOptions["users"]>([]);
   const [enteredAvailability, setEnteredAvailability] = useState();
   const [showLocationModal, setShowLocationModal] = useState(false);
   const [showAddCustomModal, setShowAddCustomModal] = useState(false);
@@ -184,12 +185,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
   const [hidden, setHidden] = useState<boolean>(eventType.hidden);
 
   const titleRef = useRef<HTMLInputElement>(null);
-  const slugRef = useRef<HTMLInputElement>(null);
-  const requiresConfirmationRef = useRef<HTMLInputElement>(null);
   const eventNameRef = useRef<HTMLInputElement>(null);
-  const periodDaysRef = useRef<HTMLInputElement>(null);
-  const periodDaysTypeRef = useRef<HTMLSelectElement>(null);
-  const priceRef = useRef<HTMLInputElement>(null);
+  const isAdvancedSettingsVisible = !!eventNameRef.current;
 
   useEffect(() => {
     setSelectedTimeZone(eventType.timeZone);
@@ -200,45 +197,47 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
 
     const formData = Object.fromEntries(new FormData(event.target).entries());
 
-    const enteredTitle: string = titleRef.current.value;
-    const enteredSlug: string = slugRef.current.value;
-    const enteredPrice = requirePayment ? Math.round(parseFloat(priceRef.current.value) * 100) : 0;
+    const enteredTitle: string = titleRef.current!.value;
 
-    const advancedOptionsPayload: AdvancedOptions = {};
-    if (requiresConfirmationRef.current) {
-      advancedOptionsPayload.eventName = eventNameRef.current.value;
-      advancedOptionsPayload.periodType = periodType.type;
-      advancedOptionsPayload.periodDays = parseInt(periodDaysRef?.current?.value);
-      advancedOptionsPayload.periodCountCalendarDays = Boolean(parseInt(periodDaysTypeRef?.current.value));
-      advancedOptionsPayload.periodStartDate = periodStartDate ? periodStartDate.toDate() : null;
-      advancedOptionsPayload.periodEndDate = periodEndDate ? periodEndDate.toDate() : null;
+    const advancedPayload: AdvancedOptions = {};
+    if (isAdvancedSettingsVisible) {
+      advancedPayload.eventName = eventNameRef.current.value;
+      advancedPayload.periodType = periodType?.type;
+      advancedPayload.periodDays = asNumberOrUndefined(formData.periodDays);
+      advancedPayload.periodCountCalendarDays = Boolean(
+        asNumberOrUndefined(formData.periodCountCalendarDays)
+      );
+      advancedPayload.periodStartDate = periodStartDate ? periodStartDate.toDate() : undefined;
+      advancedPayload.periodEndDate = periodEndDate ? periodEndDate.toDate() : undefined;
+      advancedPayload.minimumBookingNotice = asNumberOrUndefined(formData.minimumBookingNotice);
+      // prettier-ignore
+      advancedPayload.price = 
+        !requirePayment ? undefined                                                     :
+        formData.price  ? Math.round(parseFloat(asStringOrThrow(formData.price)) * 100) :
+        /* otherwise */   0;
+      advancedPayload.currency = currency;
     }
 
     const payload: EventTypeInput = {
       id: eventType.id,
       title: enteredTitle,
-      slug: enteredSlug,
-      description: formData.description as string,
-      // note(zomars) Why does this field doesnt need to be parsed...
-      length: formData.length as unknown as number,
-      // note(zomars) ...But this does? (Is being sent as string, despite it's a number field)
-      minimumBookingNotice: parseInt(formData.minimumBookingNotice as unknown as string),
+      slug: asStringOrThrow(formData.slug),
+      description: asStringOrThrow(formData.description),
+      length: asNumberOrThrow(formData.length),
       requiresConfirmation: formData.requiresConfirmation === "on",
       disableGuests: formData.disableGuests === "on",
       hidden,
       locations,
       customInputs,
       timeZone: selectedTimeZone,
-      availability: enteredAvailability || null,
-      ...advancedOptionsPayload,
+      availability: enteredAvailability || undefined,
+      ...advancedPayload,
       ...(team
         ? {
-            schedulingType: formData.schedulingType as string,
+            schedulingType: formData.schedulingType as SchedulingType,
             users,
           }
         : {}),
-      price: enteredPrice,
-      currency: currency,
     };
 
     updateMutation.mutate(payload);
@@ -411,7 +410,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
                           {team ? "team/" + team.slug : eventType.users[0].username}/
                         </span>
                         <input
-                          ref={slugRef}
                           type="text"
                           name="slug"
                           id="slug"
@@ -727,7 +725,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
                         </div>
 
                         <CheckboxField
-                          ref={requiresConfirmationRef}
                           id="requiresConfirmation"
                           name="requiresConfirmation"
                           label="Opt-in booking"
@@ -800,7 +797,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
                                             {period.type === "rolling" && (
                                               <div className="inline-flex">
                                                 <input
-                                                  ref={periodDaysRef}
                                                   type="text"
                                                   name="periodDays"
                                                   id=""
@@ -809,7 +805,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
                                                   defaultValue={eventType.periodDays || 30}
                                                 />
                                                 <select
-                                                  ref={periodDaysTypeRef}
                                                   id=""
                                                   name="periodDaysType"
                                                   className="block w-full py-2 pl-3 pr-10 text-base border-gray-300 rounded-sm  focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
@@ -924,7 +919,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
                                       <div className="w-full">
                                         <div className="mt-1 relative rounded-sm shadow-sm">
                                           <input
-                                            ref={priceRef}
                                             type="number"
                                             name="price"
                                             id="price"
@@ -1200,6 +1194,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
     };
   }
 
+  const userSelect = Prisma.validator<Prisma.UserSelect>()({
+    name: true,
+    id: true,
+    avatar: true,
+    email: true,
+  });
+
   const eventType = await prisma.eventType.findFirst({
     where: {
       AND: [
@@ -1251,24 +1252,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
             },
             select: {
               user: {
-                select: {
-                  name: true,
-                  id: true,
-                  avatar: true,
-                  email: true,
-                },
+                select: userSelect,
               },
             },
           },
         },
       },
       users: {
-        select: {
-          name: true,
-          id: true,
-          avatar: true,
-          username: true,
-        },
+        select: userSelect,
       },
       schedulingType: true,
       userId: true,
@@ -1284,17 +1275,15 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
   }
 
   // backwards compat
-  if (eventType.users.length === 0) {
-    eventType.users.push(
-      await prisma.user.findUnique({
-        where: {
-          id: session.user.id,
-        },
-        select: {
-          username: true,
-        },
-      })
-    );
+  if (eventType.users.length === 0 && !eventType.team) {
+    const fallbackUser = await prisma.user.findUnique({
+      where: {
+        id: session.user.id,
+      },
+      select: userSelect,
+    });
+    if (!fallbackUser) throw Error("The event type doesn't have user and no fallback user was found");
+    eventType.users.push(fallbackUser);
   }
 
   const credentials = await prisma.credential.findMany({
@@ -1321,7 +1310,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
     locationOptions.push({ value: LocationType.GoogleMeet, label: "Google Meet" });
   }
   const currency =
-    (credentials.find((integration) => integration.type === "stripe_payment")?.key as Stripe.OAuthToken)
+    (credentials.find((integration) => integration.type === "stripe_payment")?.key as unknown as StripeData)
       ?.default_currency || "usd";
 
   if (hasIntegration(integrations, "office365_calendar")) {
diff --git a/pages/getting-started.tsx b/pages/getting-started.tsx
index 2ea1a220..be9968ce 100644
--- a/pages/getting-started.tsx
+++ b/pages/getting-started.tsx
@@ -24,7 +24,7 @@ import { getSession } from "@lib/auth";
 import AddCalDavIntegration, {
   ADD_CALDAV_INTEGRATION_FORM_TITLE,
 } from "@lib/integrations/CalDav/components/AddCalDavIntegration";
-import { validJson } from "@lib/jsonUtils";
+import getIntegrations from "@lib/integrations/getIntegrations";
 import prisma from "@lib/prisma";
 
 import { Dialog, DialogClose, DialogContent, DialogHeader } from "@components/Dialog";
@@ -688,40 +688,7 @@ export async function getServerSideProps(context: NextPageContext) {
     },
   });
 
-  integrations = [
-    {
-      installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
-      credential: credentials.find((integration) => integration.type === "google_calendar") || null,
-      type: "google_calendar",
-      title: "Google Calendar",
-      imageSrc: "integrations/google-calendar.svg",
-      description: "Gmail, G Suite",
-    },
-    {
-      installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET),
-      credential: credentials.find((integration) => integration.type === "office365_calendar") || null,
-      type: "office365_calendar",
-      title: "Office 365 Calendar",
-      imageSrc: "integrations/outlook.svg",
-      description: "Office 365, Outlook.com, live.com, or hotmail calendar",
-    },
-    {
-      installed: !!(process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET),
-      credential: credentials.find((integration) => integration.type === "zoom_video") || null,
-      type: "zoom_video",
-      title: "Zoom",
-      imageSrc: "integrations/zoom.svg",
-      description: "Video Conferencing",
-    },
-    {
-      installed: true,
-      credential: credentials.find((integration) => integration.type === "caldav_calendar") || null,
-      type: "caldav_calendar",
-      title: "Caldav",
-      imageSrc: "integrations/caldav.svg",
-      description: "CalDav Server",
-    },
-  ];
+  integrations = getIntegrations(credentials);
 
   eventTypes = await prisma.eventType.findMany({
     where: {
@@ -748,6 +715,7 @@ export async function getServerSideProps(context: NextPageContext) {
 
   return {
     props: {
+      session,
       user,
       integrations,
       eventTypes,