From 6c863170815c37b9a9ae519caa09ab2a21ebf23a Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Fri, 26 Aug 2022 06:18:50 +0530 Subject: [PATCH] AppStore CLI: Making video app creation a breeze with major cleanup of locations code throughout (#3825) * Fix breadcrumb colors * HorizontalTabs * Team List Item WIP * Horizontal Tabs * Cards * Remove team list item WIP * Login Page * Add welcome back i118n * EventType page work * Update EventType Icons * WIP Availability * Horizontal Tab Work * Add build command for in root * Update build DIr/command * Add Edit Button + change buttons to v2 * Availablitiy page * Fix IPAD * Make mobile look a little nicer * WIP bookingshell * Remove list items from breaking build * Add Embed ModalBox for routing forms * Mian bulk of Booking Page. * Few updates to components * Fix chormatic feedback * Add duplicate form support * Fix duplication logic * Change to feathericons everywhere and other fixes * Dont allow routes for fallback route * Fix banner * Fix Empty Screen * Text area + embded window fixes * Semi fix avatar * Fix all TS issues * Fix tests * Troubleshoot container + Active on count * Support routing using query params * Improve mobile * NITS * Fix padding on input * Support multiselect in router endpoint * Fix the issue where app goes in embed mode after viewing embed once * Fix icons * Add router url tests * Add Responses download and form toggling tests * Add required validation test * Change Icons everywhere * App typeform app * Improvements in cli * Starting to move event types settings to tabs * Begin migration to single page form * Single page tabs * Limits Page * Advanced tab * Add RHF to dependancies * Add typeform how-to-use page * Add typeform how-to-use page and screenshots * Most of advanced tab * Solved RHF mismtach * Build fixes * RHF conditionals fixes * Improved legibility * Fix TS error * Add missing image * Update CliApp.tsx * Major refactor/organisation into optional V2 UI * Portal EditLocationModal * Fix dialoug form * Update imports * Auto Animate + custom inputs WIP * Custom Inputs * WIP Apps * Fixing stories imports * Stripe app * Remove duplicate dialog * Remove duplicate dialog * Major locations cleanup, 10s of bug fixes and app-store improvements * Fix missing pieces * More fixes * Fix embed URL * Fix app toggles + number of active apps * Fix container padding on disabledBorder prop * Removes strict * more fixes * EventType Team page WIP * Fix embed * NIT * Add Darkmode gray color * V2 Shell WIP * Fix headings on shell V2 * Fix mobile layout with V2 shell * V2 create event type button * Checked Team Select * Hidden to happen on save - not on toggle * Team Attendee Select animation * Fix scheduling type and remove multi select label * Fix overflow on teams url * Revert console * Revert api * Fix Embed TS errors * Fix TS errors * Fix Eslint errors * Fix TS errors for UI * Fix ESLINT error * Fix TS errors * Add missing import * Fix CLI * Add a default placeholder * Remove hardcoded daily:integrations * Fix message for payment page * Revert api and console to main * Update README * Fix TS errors * Fix Lint warnings * Fix Tests * Fix conflict issues * Fix conflict issues Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Peer Richelsen Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars --- .../booking/AvailableEventLocations.tsx | 32 ++ .../components/booking/BookingListItem.tsx | 28 +- .../booking/pages/AvailabilityPage.tsx | 111 +--- .../components/booking/pages/BookingPage.tsx | 139 +++-- .../components/dialog/EditLocationDialog.tsx | 477 +++++------------- .../v2/eventtype/EditLocationDialog.tsx | 462 +++++------------ .../components/v2/eventtype/EventSetupTab.tsx | 408 +++------------ apps/web/lib/linkValueToString.ts | 41 -- apps/web/pages/[user]/[type].tsx | 9 +- apps/web/pages/[user]/book.tsx | 8 +- apps/web/pages/api/book/event.ts | 13 +- apps/web/pages/api/cancel.ts | 3 +- apps/web/pages/apps/[slug]/index.tsx | 2 +- apps/web/pages/d/[link]/[slug].tsx | 4 +- apps/web/pages/d/[link]/book.tsx | 6 +- apps/web/pages/event-types/[type].tsx | 390 ++------------ apps/web/pages/success.tsx | 34 +- apps/web/pages/team/[slug]/[type].tsx | 6 +- apps/web/pages/team/[slug]/book.tsx | 7 +- .../web/pages/v2/event-types/[type]/index.tsx | 4 +- apps/web/public/globe.svg | 1 + apps/web/public/map-pin.svg | 1 + apps/web/public/phone.svg | 1 + apps/web/public/static/locales/en/common.json | 2 +- packages/app-store-cli/readme.md | 5 +- packages/app-store-cli/src/CliApp.tsx | 179 ++++--- packages/app-store-cli/src/app-store.ts | 1 + packages/app-store/_appRegistry.ts | 9 +- packages/app-store/_baseApp/_metadata.ts | 10 +- packages/app-store/_example/_metadata.ts | 4 +- packages/app-store/applecalendar/_metadata.ts | 4 +- packages/app-store/around/_metadata.ts | 22 +- .../app-store/caldavcalendar/_metadata.ts | 4 +- packages/app-store/dailyvideo/_metadata.ts | 14 +- .../app-store/ee/routing_forms/_metadata.ts | 4 +- .../exchange2013calendar/_metadata.ts | 4 +- .../exchange2016calendar/_metadata.ts | 4 +- packages/app-store/giphy/_metadata.ts | 4 +- .../app-store/googlecalendar/_metadata.ts | 5 +- packages/app-store/googlevideo/_metadata.ts | 14 +- .../hubspotothercalendar/_metadata.ts | 4 +- packages/app-store/huddle01video/_metadata.ts | 14 +- packages/app-store/jitsivideo/_metadata.ts | 14 +- packages/app-store/larkcalendar/_metadata.ts | 4 +- packages/app-store/locations.ts | 356 +++++++++++-- packages/app-store/metamask/_metadata.ts | 4 +- .../app-store/office365calendar/_metadata.ts | 4 +- .../app-store/office365video/_metadata.ts | 14 +- packages/app-store/ping/_metadata.ts | 16 +- packages/app-store/riverside/_metadata.ts | 16 +- .../app-store/slackmessaging/_metadata.ts | 4 +- .../slackmessaging/lib/actions/createEvent.ts | 3 +- packages/app-store/stripepayment/_metadata.ts | 4 +- packages/app-store/tandemvideo/_metadata.ts | 14 +- packages/app-store/typeform/_metadata.ts | 10 +- packages/app-store/utils.ts | 63 +-- packages/app-store/vital/_metadata.ts | 4 +- packages/app-store/whereby/_metadata.ts | 16 +- .../app-store/wipemycalother/_metadata.ts | 4 +- packages/app-store/zapier/_metadata.ts | 4 +- .../app-store/zapier/pages/setup/index.tsx | 4 +- packages/app-store/zoomvideo/_metadata.ts | 16 +- packages/core/EventManager.ts | 49 +- {apps/web/lib => packages/core}/event.ts | 33 +- .../emails/src/components/LocationInfo.tsx | 108 ++-- .../ee/payments/components/PaymentPage.tsx | 4 +- .../lib/reminders/templates/customTemplate.ts | 34 +- packages/lib/defaultEvents.ts | 3 +- packages/lib/server/defaultResponder.ts | 1 + packages/prisma/seed-app-store.config.json | 26 +- packages/prisma/seed.ts | 9 +- packages/prisma/zod-utils.ts | 4 +- packages/trpc/server/routers/viewer.tsx | 3 +- .../trpc/server/routers/viewer/bookings.tsx | 4 +- .../trpc/server/routers/viewer/eventTypes.tsx | 3 +- packages/types/App.d.ts | 50 +- packages/ui/v2/core/Card.tsx | 1 - yarn.lock | 4 +- 78 files changed, 1275 insertions(+), 2129 deletions(-) create mode 100644 apps/web/components/booking/AvailableEventLocations.tsx delete mode 100644 apps/web/lib/linkValueToString.ts create mode 100644 apps/web/public/globe.svg create mode 100644 apps/web/public/map-pin.svg create mode 100644 apps/web/public/phone.svg rename {apps/web/lib => packages/core}/event.ts (57%) diff --git a/apps/web/components/booking/AvailableEventLocations.tsx b/apps/web/components/booking/AvailableEventLocations.tsx new file mode 100644 index 0000000000..c46da9f75b --- /dev/null +++ b/apps/web/components/booking/AvailableEventLocations.tsx @@ -0,0 +1,32 @@ +import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; + +import { Props } from "./pages/AvailabilityPage"; + +export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) { + return ( +
+
+

+ {locations.map((location) => { + const eventLocationType = getEventLocationType(location.type); + if (!eventLocationType) { + // It's possible that the location app got uninstalled + return null; + } + return ( + + {`${eventLocationType.label} + {locationKeyToString(location)} + + ); + })} +

+
+
+ ); +} diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 3bbc90570a..705eb91746 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import { useState } from "react"; import { useMutation } from "react-query"; +import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations"; import dayjs from "@calcom/dayjs"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -17,8 +18,6 @@ import { TextArea } from "@calcom/ui/form/fields"; import { HttpError } from "@lib/core/http/error"; import useMeQuery from "@lib/hooks/useMeQuery"; -import { linkValueToString } from "@lib/linkValueToString"; -import { LocationType } from "@lib/location"; import { extractRecurringDates } from "@lib/parseDate"; import { EditLocationDialog } from "@components/dialog/EditLocationDialog"; @@ -182,17 +181,10 @@ function BookingListItem(booking: BookingItemProps) { }, }); - const saveLocation = (newLocationType: LocationType, details: { [key: string]: string }) => { + const saveLocation = (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => { let newLocation = newLocationType as string; - if ( - newLocationType === LocationType.InPerson || - newLocationType === LocationType.Link || - newLocationType === LocationType.UserPhone || - newLocationType === LocationType.Riverside || - newLocationType === LocationType.Around || - newLocationType === LocationType.Whereby || - newLocationType === LocationType.Ping - ) { + const eventLocationType = getEventLocationType(newLocationType); + if (eventLocationType?.organizerInputType) { newLocation = details[Object.keys(details)[0]]; } setLocationMutation.mutate({ bookingId: booking.id, newLocation }); @@ -215,17 +207,7 @@ function BookingListItem(booking: BookingItemProps) { } } - let location = booking.location || ""; - - if (location.includes("integration")) { - if (booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED) { - location = t("web_conference"); - } else if (isConfirmed) { - location = linkValueToString(booking.location, t); - } else { - location = t("web_conferencing_details_to_follow"); - } - } + const location = booking.location || ""; const onClick = () => { router.push({ diff --git a/apps/web/components/booking/pages/AvailabilityPage.tsx b/apps/web/components/booking/pages/AvailabilityPage.tsx index e213d5eedd..e876e118cf 100644 --- a/apps/web/components/booking/pages/AvailabilityPage.tsx +++ b/apps/web/components/booking/pages/AvailabilityPage.tsx @@ -8,7 +8,6 @@ import { useEffect, useMemo, useState } from "react"; import { FormattedNumber, IntlProvider } from "react-intl"; import { z } from "zod"; -import { AppStoreLocationType, LocationObject, LocationType } from "@calcom/app-store/locations"; import dayjs, { Dayjs } from "@calcom/dayjs"; import { useEmbedNonStylesConfig, @@ -46,41 +45,9 @@ import PoweredByCal from "@components/ui/PoweredByCal"; import type { AvailabilityPageProps } from "../../../pages/[user]/[type]"; import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]"; import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]"; +import { AvailableEventLocations } from "../AvailableEventLocations"; -type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps; - -export const locationKeyToString = (location: LocationObject, t: TFunction) => { - switch (location.type) { - case LocationType.InPerson: - return location.address || "In Person"; // If disabled address won't exist on the object - case LocationType.Link: - case LocationType.Ping: - case LocationType.Riverside: - case LocationType.Around: - case LocationType.Whereby: - return location.link || "Link"; // If disabled link won't exist on the object - case LocationType.Phone: - return t("your_number"); - case LocationType.UserPhone: - return t("phone_call"); - case LocationType.GoogleMeet: - return "Google Meet"; - case LocationType.Zoom: - return "Zoom"; - case LocationType.Daily: - return "Cal Video"; - case LocationType.Jitsi: - return "Jitsi"; - case LocationType.Huddle01: - return "Huddle Video"; - case LocationType.Tandem: - return "Tandem"; - case LocationType.Teams: - return "Microsoft Teams"; - default: - return null; - } -}; +export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps; const GoBackToPreviousPage = ({ t }: { t: TFunction }) => { const router = useRouter(); @@ -457,40 +424,9 @@ const AvailabilityPage = ({ profile, eventType }: Props) => { {t("requires_confirmation")}

)} - {eventType.locations.length === 1 && ( -

- {Object.values(AppStoreLocationType).includes( - eventType.locations[0].type as unknown as AppStoreLocationType - ) ? ( - - ) : ( - - )} - - {locationKeyToString(eventType.locations[0], t)} -

- )} - {eventType.locations.length > 1 && ( -
-
- -
-

- {eventType.locations.map((el, i, arr) => { - return ( - - {locationKeyToString(el, t)}{" "} - {arr.length - 1 !== i && ( - {t("or_lowercase")} - )} - - ); - })} -

-
- )} -

- + +

+ {eventType.length} {t("minutes")}

{eventType.price > 0 && ( @@ -590,40 +526,9 @@ const AvailabilityPage = ({ profile, eventType }: Props) => { {t("requires_confirmation")} )} - {eventType.locations.length === 1 && ( -

- {Object.values(AppStoreLocationType).includes( - eventType.locations[0].type as unknown as AppStoreLocationType - ) ? ( - - ) : ( - - )} - - {locationKeyToString(eventType.locations[0], t)} -

- )} - {eventType.locations.length > 1 && ( -
-
- -
-

- {eventType.locations.map((el, i, arr) => { - return ( - - {locationKeyToString(el, t)}{" "} - {arr.length - 1 !== i && ( - {t("or_lowercase")} - )} - - ); - })} -

-
- )} -

- + +

+ {eventType.length} {t("minutes")}

{!rescheduleUid && eventType.recurringEvent && ( diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index bff0cdb89d..f90a5f34d1 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -13,7 +13,14 @@ import { useMutation } from "react-query"; import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; +import { + locationKeyToString, + getEventLocationValue, + getEventLocationType, + EventLocationType, +} from "@calcom/app-store/locations"; import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client"; +import { LocationObject, LocationType } from "@calcom/core/location"; import dayjs from "@calcom/dayjs"; import { useEmbedNonStylesConfig, @@ -37,7 +44,6 @@ import { EmailInput, Form } from "@calcom/ui/form/fields"; import { asStringOrNull } from "@lib/asStringOrNull"; import { timeZone } from "@lib/clock"; import { ensureArray } from "@lib/ensureArray"; -import { LocationObject, LocationType } from "@lib/location"; import createBooking from "@lib/mutations/bookings/create-booking"; import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking"; import { parseDate, parseRecurringDates } from "@lib/parseDate"; @@ -58,15 +64,13 @@ declare global { }; } -type BookingPageProps = (BookPageProps | TeamBookingPageProps | HashLinkPageProps) & { - locationLabels: Record; -}; +type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps; type BookingFormValues = { name: string; email: string; notes?: string; - locationType?: LocationType; + locationType?: EventLocationType["type"]; guests?: string[]; phone?: string; hostPhoneNumber?: string; // Maybe come up with a better way to name this to distingish between two types of phone numbers @@ -83,7 +87,6 @@ const BookingPage = ({ profile, isDynamicGroupBooking, recurringEventCount, - locationLabels, hasHashedBookingLink, hashedLink, }: BookingPageProps) => { @@ -132,16 +135,6 @@ const BookingPage = ({ ); } - const location = (function humanReadableLocation(location) { - if (!location) { - return; - } - if (location.includes("integration")) { - return t("web_conferencing_details_to_follow"); - } - return location; - })(responseData.location); - return router.push({ pathname: "/success", query: { @@ -152,7 +145,7 @@ const BookingPage = ({ reschedule: !!rescheduleUid, name: attendees[0].name, email: attendees[0].email, - location, + location: responseData.location, eventName: profile.eventName || "", bookingId: id, isSuccessBookingPage: true, @@ -213,7 +206,6 @@ const BookingPage = ({ } }, [router.query.guest]); - const locationInfo = (type: LocationType) => locations.find((location) => location.type === type); const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id; const guestListEmails = !isDynamicGroupBooking ? booking?.attendees.slice(1).map((attendee) => attendee.email) @@ -296,12 +288,12 @@ const BookingPage = ({ resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema }); - const selectedLocation = useWatch({ + const selectedLocationType = useWatch({ control: bookingForm.control, name: "locationType", - defaultValue: ((): LocationType | undefined => { + defaultValue: ((): EventLocationType["type"] | undefined => { if (router.query.location) { - return router.query.location as LocationType; + return router.query.location as EventLocationType["type"]; } if (locations.length === 1) { return locations[0]?.type; @@ -309,40 +301,13 @@ const BookingPage = ({ })(), }); - const getLocationValue = ( - booking: Pick - ) => { - const { locationType } = booking; - switch (locationType) { - case LocationType.Phone: { - return booking.phone || ""; - } - case LocationType.InPerson: { - return locationInfo(locationType)?.address || ""; - } - case LocationType.Link: { - return locationInfo(locationType)?.link || ""; - } - case LocationType.UserPhone: { - return locationInfo(locationType)?.hostPhoneNumber || ""; - } - case LocationType.Around: { - return locationInfo(locationType)?.link || ""; - } - case LocationType.Riverside: { - return locationInfo(locationType)?.link || ""; - } - case LocationType.Whereby: { - return locationInfo(locationType)?.link || ""; - } - case LocationType.Ping: { - return locationInfo(locationType)?.link || ""; - } - // Catches all other location types, such as Google Meet, Zoom etc. - default: - return selectedLocation || ""; - } - }; + const selectedLocation = getEventLocationType(selectedLocationType); + const AttendeeInput = + selectedLocation?.attendeeInputType === "text" + ? "input" + : selectedLocation?.attendeeInputType === "phone" + ? PhoneInput + : null; // Calculate the booking date(s) let recurringStrings: string[] = [], @@ -403,9 +368,10 @@ const BookingPage = ({ language: i18n.language, rescheduleUid, user: router.query.user, - location: getLocationValue( - booking.locationType ? booking : { ...booking, locationType: selectedLocation } - ), + location: getEventLocationValue(locations, { + type: booking.locationType ? booking.locationType : selectedLocationType || "", + phone: booking.phone, + }), metadata, customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({ label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "", @@ -414,7 +380,7 @@ const BookingPage = ({ hasHashedBookingLink, hashedLink, smsReminderNumber: - selectedLocation === LocationType.Phone ? booking.phone : booking.smsReminderNumber, + selectedLocationType === LocationType.Phone ? booking.phone : booking.smsReminderNumber, })); recurringMutation.mutate(recurringBookings); } else { @@ -430,9 +396,10 @@ const BookingPage = ({ rescheduleUid, bookingUid: router.query.bookingUid as string, user: router.query.user, - location: getLocationValue( - booking.locationType ? booking : { ...booking, locationType: selectedLocation } - ), + location: getEventLocationValue(locations, { + type: (booking.locationType ? booking.locationType : selectedLocationType) || "", + phone: booking.phone, + }), metadata, customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({ label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "", @@ -441,13 +408,14 @@ const BookingPage = ({ hasHashedBookingLink, hashedLink, smsReminderNumber: - selectedLocation === LocationType.Phone ? booking.phone : booking.smsReminderNumber, + selectedLocationType === LocationType.Phone ? booking.phone : booking.smsReminderNumber, }); } }; // Should be disabled when rescheduleUid is present and data was found in defaultUserValues name/email fields. const disableInput = !!rescheduleUid && !!defaultUserValues.email && !!defaultUserValues.name; + const disableLocations = !!rescheduleUid; const disabledExceptForOwner = disableInput && !loggedInIsOwner; const inputClassName = "dark:placeholder:text-darkgray-600 focus:border-brand dark:border-darkgray-300 dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500"; @@ -656,21 +624,34 @@ const BookingPage = ({ {t("location")} - {locations.map((location, i) => ( - - ))} + {locations.map((location, i) => { + const locationString = locationKeyToString(location); + // TODO: Right now selectedLocationType isn't send by getSSP. Once that's available defaultChecked should work and show the location in the original booking + const defaultChecked = rescheduleUid ? selectedLocationType === location.type : i === 0; + if (typeof locationString !== "string") { + // It's possible that location app got uninstalled + return null; + } + return ( + + ); + })} )} - {selectedLocation === LocationType.Phone && ( + {/* TODO: Change name and id ="phone" to something generic */} + {AttendeeInput && (