Merge branch 'main' into fix/after-meeting-ends-migration

This commit is contained in:
kodiakhq[bot] 2022-08-26 00:52:27 +00:00 committed by GitHub
commit 5a502638b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 1275 additions and 2129 deletions

View File

@ -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 (
<div>
<div className="flex-warp mr-6 flex break-words text-sm text-gray-600 dark:text-white">
<p className="w-full">
{locations.map((location) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
// It's possible that the location app got uninstalled
return null;
}
return (
<span key={location.type} className="flex flex-row items-center pt-1">
<img
src={eventLocationType.iconUrl}
className="mr-[10px] ml-[2px] h-4 w-4"
alt={`${eventLocationType.label} icon`}
/>
<span key={location.type}>{locationKeyToString(location)} </span>
</span>
);
})}
</p>
</div>
</div>
);
}

View File

@ -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({

View File

@ -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")}
</p>
)}
{eventType.locations.length === 1 && (
<p className="dark:text-darkgray-600 text-gray-600">
{Object.values(AppStoreLocationType).includes(
eventType.locations[0].type as unknown as AppStoreLocationType
) ? (
<Icon.FiVideo className="dark:text-darkgray-600 mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
) : (
<Icon.FiMapPin className="dark:text-darkgray-600 mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
)}
{locationKeyToString(eventType.locations[0], t)}
</p>
)}
{eventType.locations.length > 1 && (
<div className="flex-warp dark:text-darkgray-600 flex text-gray-600">
<div className="mr-[10px] ml-[2px] -mt-1 ">
<Icon.FiMapPin className="dark:text-darkgray-600 inline-block h-4 w-4 text-gray-500" />
</div>
<p>
{eventType.locations.map((el, i, arr) => {
return (
<span key={el.type}>
{locationKeyToString(el, t)}{" "}
{arr.length - 1 !== i && (
<span className="font-light"> {t("or_lowercase")} </span>
)}
</span>
);
})}
</p>
</div>
)}
<p className="dark:text-darkgray-600 text-gray-600">
<Icon.FiClock className="dark:text-darkgray-600 mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-500" />
<AvailableEventLocations locations={eventType.locations} />
<p className="text-gray-600 dark:text-white">
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-500" />
{eventType.length} {t("minutes")}
</p>
{eventType.price > 0 && (
@ -590,40 +526,9 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
{t("requires_confirmation")}
</div>
)}
{eventType.locations.length === 1 && (
<p className="dark:text-darkgray-600 py-1 text-sm font-medium text-gray-600">
{Object.values(AppStoreLocationType).includes(
eventType.locations[0].type as unknown as AppStoreLocationType
) ? (
<Icon.FiVideo className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " />
) : (
<Icon.FiMapPin className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " />
)}
{locationKeyToString(eventType.locations[0], t)}
</p>
)}
{eventType.locations.length > 1 && (
<div className="flex-warp dark:text-darkgray-600 flex items-center font-medium text-gray-600">
<div className="mr-[10px] ml-[2px] -mt-1 ">
<Icon.FiMapPin className="inline-block h-4 w-4 " />
</div>
<p className="dark:text-darkgray-600 py-1 text-sm font-medium ">
{eventType.locations.map((el, i, arr) => {
return (
<span key={el.type}>
{locationKeyToString(el, t)}{" "}
{arr.length - 1 !== i && (
<span className="text-sm font-light"> {t("or_lowercase")} </span>
)}
</span>
);
})}
</p>
</div>
)}
<p className="dark:text-darkgray-600 py-1 text-sm font-medium text-gray-600">
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 " />
<AvailableEventLocations locations={eventType.locations} />
<p className="py-1 text-sm font-medium text-gray-600 dark:text-white">
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-500" />
{eventType.length} {t("minutes")}
</p>
{!rescheduleUid && eventType.recurringEvent && (

View File

@ -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<LocationType, string>;
};
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<BookingFormValues, "locationType" | "phone" | "hostPhoneNumber">
) => {
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 = ({
<span className="block text-sm font-medium text-gray-700 dark:text-white">
{t("location")}
</span>
{locations.map((location, i) => (
<label key={i} className="block">
<input
type="radio"
className="location h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
{...bookingForm.register("locationType", { required: true })}
value={location.type}
defaultChecked={selectedLocation === location.type}
/>
<span className="text-sm ltr:ml-2 rtl:mr-2 ">{locationLabels[location.type]}</span>
</label>
))}
{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 (
<label key={i} className="block">
<input
type="radio"
disabled={!!disableLocations}
className="location h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
{...bookingForm.register("locationType", { required: true })}
value={location.type}
defaultChecked={defaultChecked}
/>
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-gray-500">
{locationKeyToString(location)}
</span>
</label>
);
})}
</div>
)}
{selectedLocation === LocationType.Phone && (
{/* TODO: Change name and id ="phone" to something generic */}
{AttendeeInput && (
<div className="mb-4">
<label
htmlFor="phone"
@ -678,10 +659,10 @@ const BookingPage = ({
{t("phone_number")}
</label>
<div className="mt-1">
<PhoneInput<BookingFormValues>
<AttendeeInput<BookingFormValues>
control={bookingForm.control}
name="phone"
placeholder={t("enter_phone_number")}
placeholder={t(selectedLocation?.attendeeInputPlaceholder || "")}
id="phone"
required
disabled={disableInput}
@ -828,7 +809,7 @@ const BookingPage = ({
)}
</div>
)}
{isSmsReminderNumberNeeded && selectedLocation !== LocationType.Phone && (
{isSmsReminderNumberNeeded && selectedLocationType !== LocationType.Phone && (
<div className="mb-4">
<label
htmlFor="smsReminderNumber"

View File

@ -1,11 +1,18 @@
import { ErrorMessage } from "@hookform/error-message";
import { zodResolver } from "@hookform/resolvers/zod";
import { isValidPhoneNumber } from "libphonenumber-js";
import { useEffect } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { z } from "zod";
import { LocationOptionsToString } from "@calcom/app-store/locations";
import { LocationType } from "@calcom/core/location";
import {
LocationType,
getEventLocationType,
EventLocationType,
LocationObject,
} from "@calcom/app-store/locations";
import { getMessageForOrganizer } from "@calcom/app-store/locations";
import { getHumanReadableLocationValue } from "@calcom/app-store/locations";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
@ -15,7 +22,6 @@ import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
import { Form } from "@calcom/ui/form/fields";
import { QueryCell } from "@lib/QueryCell";
import { linkValueToString } from "@lib/linkValueToString";
import CheckboxField from "@components/ui/form/CheckboxField";
import Select from "@components/ui/form/Select";
@ -24,33 +30,46 @@ type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
type OptionTypeBase = {
label: string;
value: LocationType;
value: EventLocationType["type"];
disabled?: boolean;
};
type LocationFormValues = {
locationType: LocationType;
locationAddress?: string;
locationLink?: string;
locationPhoneNumber?: string;
displayLocationPublicly?: boolean;
};
interface ISetLocationDialog {
saveLocation: (newLocationType: LocationType, details: { [key: string]: string }) => void;
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
selection?: OptionTypeBase;
booking?: BookingItem;
defaultValues?: {
type: LocationType;
address?: string | undefined;
link?: string | undefined;
hostPhoneNumber?: string | undefined;
displayLocationPublicly?: boolean | undefined;
}[];
defaultValues?: LocationObject[];
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
isOpenDialog: boolean;
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
}
const LocationInput = (props: {
eventLocationType: EventLocationType;
locationFormMethods: ReturnType<typeof useForm>;
id: string;
required: boolean;
placeholder: string;
className?: string;
defaultValue?: string;
}): JSX.Element | null => {
const { eventLocationType, locationFormMethods, ...remainingProps } = props;
if (eventLocationType?.organizerInputType === "text") {
return (
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
);
} else if (eventLocationType?.organizerInputType === "phone") {
return (
<PhoneInput
name={eventLocationType.variable}
control={locationFormMethods.control}
{...remainingProps}
/>
);
}
return null;
};
export const EditLocationDialog = (props: ISetLocationDialog) => {
const {
saveLocation,
@ -73,37 +92,52 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
const locationFormSchema = z.object({
locationType: z.string(),
phone: z.string().optional().nullable(),
locationAddress: z.string().optional(),
locationLink:
selection?.value === LocationType.Whereby
? z
.string()
.regex(/^http(s)?:\/\/(www\.)?whereby.com\/[a-zA-Z0-9]*/)
.optional()
: selection?.value === LocationType.Around
? z
.string()
.regex(/^http(s)?:\/\/(www\.)?around.co\/[a-zA-Z0-9]*/)
.optional()
: selection?.value === LocationType.Ping
? z
.string()
.regex(/^http(s)?:\/\/(www\.)?ping.gg\/call\/[a-zA-Z0-9]*/)
.optional()
: selection?.value === LocationType.Riverside
? z
.string()
.regex(/^http(s)?:\/\/(www\.)?riverside.fm\/studio\/[a-zA-Z0-9]*/)
.optional()
: z.string().url().optional(),
locationLink: z
.string()
.optional()
.superRefine((val, ctx) => {
if (
eventLocationType &&
!eventLocationType.default &&
eventLocationType.linkType === "static" &&
eventLocationType.urlRegExp
) {
const valid = z.string().regex(new RegExp(eventLocationType.urlRegExp)).safeParse(val).success;
if (!valid) {
const sampleUrl = eventLocationType.organizerInputPlaceholder;
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid URL for ${eventLocationType.label}. ${
sampleUrl ? "Sample URL: " + sampleUrl : ""
}`,
});
}
return;
}
const valid = z.string().url().optional().safeParse(val).success;
if (!valid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid URL`,
});
}
return;
}),
displayLocationPublicly: z.boolean().optional(),
locationPhoneNumber: z
.string()
.refine((val) => isValidPhoneNumber(val))
.nullable()
.refine((val) => {
if (val === null) return false;
return isValidPhoneNumber(val);
})
.optional(),
});
const locationFormMethods = useForm<LocationFormValues>({
const locationFormMethods = useForm({
mode: "onSubmit",
resolver: zodResolver(locationFormSchema),
});
@ -113,28 +147,43 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
name: "locationType",
});
const LocationOptions =
selectedLocation === LocationType.InPerson ? (
<>
const eventLocationType = getEventLocationType(selectedLocation);
const defaultLocation = defaultValues?.find(
(location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type
);
const LocationOptions = (() => {
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
if (!eventLocationType.variable) {
console.error("eventLocationType.variable can't be undefined");
return null;
}
return (
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_address_place")}
<label htmlFor="locationInput" className="block text-sm font-medium text-gray-700">
{t(eventLocationType.messageForOrganizer || "")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationAddress")}
id="address"
<LocationInput
locationFormMethods={locationFormMethods}
eventLocationType={eventLocationType}
id="locationInput"
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
required
className="block w-full rounded-sm border-gray-300 text-sm"
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.InPerson
)?.address
: undefined
defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined
}
/>
<ErrorMessage
errors={locationFormMethods.formState.errors}
name={eventLocationType.variable}
className="mt-1 text-sm text-red-500"
as="p"
/>
</div>
{!booking && (
<div className="mt-3">
@ -143,12 +192,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.InPerson)
?.displayLocationPublicly
: undefined
}
defaultChecked={defaultLocation?.displayLocationPublicly}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
@ -160,286 +204,11 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Link ? (
<div>
<label htmlFor="link" className="block text-sm font-medium text-gray-700">
{t("set_link_meeting")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
required
id="link"
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Link
)?.link
: undefined
}
/>
{locationFormMethods.formState.errors.locationLink && (
<p className="mt-1 text-sm text-red-500">{t("url_start_with_https")}</p>
)}
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
description={t("display_location_label")}
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Link)
?.displayLocationPublicly
: undefined
}
onChange={(e) => locationFormMethods.setValue("displayLocationPublicly", e.target.checked)}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
) : selectedLocation === LocationType.UserPhone ? (
<div>
<label htmlFor="phonenumber" className="block text-sm font-medium text-gray-700">
{t("set_your_phone_number")}
{locationFormMethods.formState?.errors?.locationPhoneNumber?.message}
</label>
<div className="mt-1">
<PhoneInput<LocationFormValues>
control={locationFormMethods.control}
name="locationPhoneNumber"
required
id="locationPhoneNumber"
placeholder={t("host_phone_number")}
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.UserPhone
)?.hostPhoneNumber
: undefined
}
/>
{locationFormMethods.formState.errors.locationPhoneNumber && (
<p className="mt-1 text-sm text-red-500">{t("invalid_number")}</p>
)}
</div>
</div>
) : selectedLocation === LocationType.Whereby ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_whereby_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="wherebylink"
placeholder="https://www.whereby.com/cal"
required
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Whereby
)?.address
: undefined
}
/>
{locationFormMethods.formState.errors.locationLink && (
<p className="mt-1 text-sm text-red-500">{t("invalid_whereby_link")}</p>
)}
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Whereby)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Around ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_around_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="aroundlink"
placeholder="https://www.around.co/rick"
required
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Around
)?.address
: undefined
}
/>
{locationFormMethods.formState.errors.locationLink && (
<p className="mt-1 text-sm text-red-500">{t("invalid_around_link")}</p>
)}
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Around)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Ping ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_ping_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="pinglink"
placeholder="https://www.ping.gg/call/theo"
required
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Ping
)?.address
: undefined
}
/>
{locationFormMethods.formState.errors.locationLink && (
<p className="mt-1 text-sm text-red-500">{t("invalid_ping_link")}</p>
)}
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Ping)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Riverside ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_riverside_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="aroundlink"
placeholder="https://www.riverside.fm/studio/rick"
required
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Riverside
)?.address
: undefined
}
/>
{locationFormMethods.formState.errors.locationLink && (
<p className="mt-1 text-sm text-red-500">{t("invalid_riverside_link")}</p>
)}
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Riverside)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : (
<p className="text-sm">{LocationOptionsToString(selectedLocation, t)}</p>
);
);
} else {
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
}
})();
return (
<Dialog open={isOpenDialog}>
@ -462,7 +231,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
{booking && (
<>
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
<p className="mb-2 ml-1 text-sm text-black">{linkValueToString(booking.location, t)}</p>
<p className="mb-2 ml-1 text-sm text-black">
{getHumanReadableLocationValue(booking.location, t)}
</p>
</>
)}
<Form
@ -474,23 +245,29 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
if (newLocation === LocationType.InPerson) {
details = {
address: values.locationAddress,
displayLocationPublicly,
};
}
const eventLocationType = getEventLocationType(newLocation);
// TODO: There can be a property that tells if it is to be saved in `link`
if (
newLocation === LocationType.Link ||
newLocation === LocationType.Whereby ||
newLocation === LocationType.Around ||
newLocation === LocationType.Riverside ||
newLocation === LocationType.Ping
(!eventLocationType?.default && eventLocationType?.linkType === "static")
) {
details = { link: values.locationLink, displayLocationPublicly };
details = { link: values.locationLink };
}
if (newLocation === LocationType.UserPhone) {
details = { hostPhoneNumber: values.locationPhoneNumber };
}
if (eventLocationType?.organizerInputType) {
details = {
...details,
displayLocationPublicly,
};
}
saveLocation(newLocation, details);
setShowLocationModal(false);
setSelectedLocation?.(undefined);

View File

@ -1,11 +1,18 @@
import { ErrorMessage } from "@hookform/error-message";
import { zodResolver } from "@hookform/resolvers/zod";
import { isValidPhoneNumber } from "libphonenumber-js";
import { useEffect } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { z } from "zod";
import { LocationOptionsToString } from "@calcom/app-store/locations";
import { LocationType } from "@calcom/core/location";
import {
LocationType,
getEventLocationType,
EventLocationType,
getHumanReadableLocationValue,
getMessageForOrganizer,
LocationObject,
} from "@calcom/app-store/locations";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
@ -15,7 +22,6 @@ import { Button } from "@calcom/ui/v2";
import { Form } from "@calcom/ui/v2";
import { QueryCell } from "@lib/QueryCell";
import { linkValueToString } from "@lib/linkValueToString";
import CheckboxField from "@components/ui/form/CheckboxField";
import Select from "@components/ui/form/Select";
@ -24,33 +30,46 @@ type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
type OptionTypeBase = {
label: string;
value: LocationType;
value: EventLocationType["type"];
disabled?: boolean;
};
type LocationFormValues = {
locationType: LocationType;
locationAddress?: string;
locationLink?: string;
locationPhoneNumber?: string;
displayLocationPublicly?: boolean;
};
interface ISetLocationDialog {
saveLocation: (newLocationType: LocationType, details: { [key: string]: string }) => void;
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
selection?: OptionTypeBase;
booking?: BookingItem;
defaultValues?: {
type: LocationType;
address?: string | undefined;
link?: string | undefined;
hostPhoneNumber?: string | undefined;
displayLocationPublicly?: boolean | undefined;
}[];
defaultValues?: LocationObject[];
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
isOpenDialog: boolean;
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
}
const LocationInput = (props: {
eventLocationType: EventLocationType;
locationFormMethods: ReturnType<typeof useForm>;
id: string;
required: boolean;
placeholder: string;
className?: string;
defaultValue?: string;
}): JSX.Element | null => {
const { eventLocationType, locationFormMethods, ...remainingProps } = props;
if (eventLocationType?.organizerInputType === "text") {
return (
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
);
} else if (eventLocationType?.organizerInputType === "phone") {
return (
<PhoneInput
name={eventLocationType.variable}
control={locationFormMethods.control}
{...remainingProps}
/>
);
}
return null;
};
export const EditLocationDialog = (props: ISetLocationDialog) => {
const {
saveLocation,
@ -73,29 +92,43 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
const locationFormSchema = z.object({
locationType: z.string(),
phone: z.string().optional(),
locationAddress: z.string().optional(),
locationLink:
selection?.value === LocationType.Whereby
? z
locationLink: z
.string()
.optional()
.superRefine((val, ctx) => {
if (
eventLocationType &&
!eventLocationType.default &&
eventLocationType.linkType === "static" &&
eventLocationType.urlRegExp
) {
const valid = z
.string()
.regex(/^http(s)?:\/\/(www\.)?whereby.com\/[a-zA-Z0-9]*/)
.optional()
: selection?.value === LocationType.Around
? z
.string()
.regex(/^http(s)?:\/\/(www\.)?around.co\/[a-zA-Z0-9]*/)
.optional()
: selection?.value === LocationType.Ping
? z
.string()
.regex(/^http(s)?:\/\/(www\.)?ping.gg\/call\/[a-zA-Z0-9]*/)
.optional()
: selection?.value === LocationType.Riverside
? z
.string()
.regex(/^http(s)?:\/\/(www\.)?riverside.fm\/studio\/[a-zA-Z0-9]*/)
.optional()
: z.string().url().optional(),
.regex(new RegExp(eventLocationType.urlRegExp || ""))
.safeParse(val).success;
if (!valid) {
const sampleUrl = eventLocationType.organizerInputPlaceholder;
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid URL for ${eventLocationType.label}. ${
sampleUrl ? "Sample URL: " + sampleUrl : ""
}`,
});
}
return;
}
const valid = z.string().url().optional().safeParse(val).success;
if (!valid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid URL`,
});
}
return;
}),
displayLocationPublicly: z.boolean().optional(),
locationPhoneNumber: z
.string()
@ -103,7 +136,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
.optional(),
});
const locationFormMethods = useForm<LocationFormValues>({
const locationFormMethods = useForm({
mode: "onSubmit",
resolver: zodResolver(locationFormSchema),
});
@ -113,28 +146,43 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
name: "locationType",
});
const LocationOptions =
selectedLocation === LocationType.InPerson ? (
<>
const eventLocationType = getEventLocationType(selectedLocation);
const defaultLocation = defaultValues?.find(
(location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type
);
const LocationOptions = (() => {
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
if (!eventLocationType.variable) {
console.error("eventLocationType.variable can't be undefined");
return null;
}
return (
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_address_place")}
<label htmlFor="locationInput" className="block text-sm font-medium text-gray-700">
{t(eventLocationType.messageForOrganizer || "")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationAddress")}
id="address"
<LocationInput
locationFormMethods={locationFormMethods}
eventLocationType={eventLocationType}
id="locationInput"
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
required
className="block w-full rounded-md border-gray-300 text-sm"
className="block w-full rounded-sm border-gray-300 text-sm"
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.InPerson
)?.address
: undefined
(defaultLocation && defaultLocation[eventLocationType.defaultValueVariable]) || ""
}
/>
<ErrorMessage
errors={locationFormMethods.formState.errors}
name={eventLocationType.variable}
className="mt-1 text-sm text-red-500"
as="p"
/>
</div>
{!booking && (
<div className="mt-3">
@ -143,12 +191,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.InPerson)
?.displayLocationPublicly
: undefined
}
defaultChecked={defaultLocation?.displayLocationPublicly}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
@ -160,274 +203,11 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Link ? (
<div>
<label htmlFor="link" className="block text-sm font-medium text-gray-700">
{t("set_link_meeting")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
required
id="link"
className="block w-full rounded-md border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Link
)?.link
: undefined
}
/>
{locationFormMethods.formState.errors.locationLink && (
<p className="mt-1 text-sm text-red-500">{t("url_start_with_https")}</p>
)}
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
description={t("display_location_label")}
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Link)
?.displayLocationPublicly
: undefined
}
onChange={(e) => locationFormMethods.setValue("displayLocationPublicly", e.target.checked)}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
) : selectedLocation === LocationType.UserPhone ? (
<div>
<label htmlFor="phonenumber" className="block text-sm font-medium text-gray-700">
{t("set_your_phone_number")}
{locationFormMethods.formState?.errors?.locationPhoneNumber?.message}
</label>
<div className="mt-1">
<PhoneInput<LocationFormValues>
control={locationFormMethods.control}
name="locationPhoneNumber"
required
id="locationPhoneNumber"
placeholder={t("host_phone_number")}
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.UserPhone
)?.hostPhoneNumber
: undefined
}
/>
{locationFormMethods.formState.errors.locationPhoneNumber && (
<p className="mt-1 text-sm text-red-500">{t("invalid_number")}</p>
)}
</div>
</div>
) : selectedLocation === LocationType.Whereby ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_whereby_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="wherebylink"
placeholder="https://www.whereby.com/cal"
required
className="block w-full rounded-md border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Whereby
)?.address
: undefined
}
/>
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Whereby)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Around ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_around_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="aroundlink"
placeholder="https://www.around.co/rick"
required
className="block w-full rounded-md border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Around
)?.address
: undefined
}
/>
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Around)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Ping ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_ping_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="pinglink"
placeholder="https://www.ping.gg/call/theo"
required
className="block w-full rounded-md border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Ping
)?.address
: undefined
}
/>
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Ping)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : selectedLocation === LocationType.Riverside ? (
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_riverside_link")}
</label>
<div className="mt-1">
<input
type="text"
{...locationFormMethods.register("locationLink")}
id="aroundlink"
placeholder="https://www.riverside.fm/studio/rick"
required
className="block w-full rounded-md border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
(location: { type: LocationType }) => location.type === LocationType.Riverside
)?.address
: undefined
}
/>
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={
defaultValues
? defaultValues.find((location) => location.type === LocationType.Riverside)
?.displayLocationPublicly
: undefined
}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
</>
) : (
<p className="text-sm">{LocationOptionsToString(selectedLocation, t)}</p>
);
);
} else {
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
}
})();
return (
<Dialog open={isOpenDialog}>
@ -450,7 +230,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
{booking && (
<>
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
<p className="mb-2 ml-1 text-sm text-black">{linkValueToString(booking.location, t)}</p>
<p className="mb-2 ml-1 text-sm text-black">
{getHumanReadableLocationValue(booking.location, t)}
</p>
</>
)}
<Form
@ -462,23 +244,29 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
if (newLocation === LocationType.InPerson) {
details = {
address: values.locationAddress,
displayLocationPublicly,
};
}
const eventLocationType = getEventLocationType(newLocation);
// TODO: There can be a property that tells if it is to be saved in `link`
if (
newLocation === LocationType.Link ||
newLocation === LocationType.Whereby ||
newLocation === LocationType.Around ||
newLocation === LocationType.Riverside ||
newLocation === LocationType.Ping
(!eventLocationType?.default && eventLocationType?.linkType === "static")
) {
details = { link: values.locationLink, displayLocationPublicly };
details = { link: values.locationLink };
}
if (newLocation === LocationType.UserPhone) {
details = { hostPhoneNumber: values.locationPhoneNumber };
}
if (eventLocationType?.organizerInputType) {
details = {
...details,
displayLocationPublicly,
};
}
saveLocation(newLocation, details);
setShowLocationModal(false);
setSelectedLocation?.(undefined);

File diff suppressed because one or more lines are too long

View File

@ -1,41 +0,0 @@
import { TFunction } from "next-i18next";
import { LocationType } from "./location";
/**
* Use this function to translate booking location value to a readable string
* @param linkValue
* @param translationFunction
* @returns
*/
export const linkValueToString = (
linkValue: string | undefined | null,
translationFunction: TFunction
): string => {
const t = translationFunction;
if (!linkValue) {
return translationFunction("no_location");
}
switch (linkValue) {
case LocationType.InPerson:
return t("in_person_meeting");
case LocationType.UserPhone:
return t("user_phone");
case LocationType.GoogleMeet:
return `Google Meet: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Zoom:
return `Zoom: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Daily:
return `Cal Video: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Jitsi:
return `Jitsi: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Huddle01:
return `Huddle01t: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Tandem:
return `Tandem: ${t("meeting_url_in_conformation_email")}`;
case LocationType.Teams:
return `Teams: ${t("meeting_url_in_conformation_email")}`;
default:
return linkValue || "";
}
};

View File

@ -3,7 +3,7 @@ import { GetStaticPaths, GetStaticPropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { z } from "zod";
import { locationHiddenFilter, LocationObject } from "@calcom/app-store/locations";
import { privacyFilteredLocations, LocationObject } from "@calcom/app-store/locations";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -148,12 +148,12 @@ async function getUserPageProps(context: GetStaticPropsContext) {
if (!eventType) return { notFound: true };
//TODO: Use zodSchema to verify it instead of using Type Assertion
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
const eventTypeObject = Object.assign({}, eventType, {
metadata: (eventType.metadata || {}) as JSONObject,
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: locationHiddenFilter(locations),
locations: privacyFilteredLocations(locations),
users: eventType.users.map((user) => ({
name: user.name,
username: user.username,
@ -241,11 +241,10 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
}
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
const eventTypeObject = Object.assign({}, eventType, {
metadata: (eventType.metadata || {}) as JSONObject,
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: locationHiddenFilter(locations),
locations: privacyFilteredLocations(locations),
users: users.map((user) => {
return {
name: user.name,

View File

@ -1,7 +1,7 @@
import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { getLocationLabels } from "@calcom/app-store/utils";
import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations";
import { parseRecurringEvent } from "@calcom/lib";
import {
getDefaultEvent,
@ -25,7 +25,6 @@ export type BookPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: BookPageProps) {
const { t } = useLocale();
const locationLabels = getLocationLabels(t);
return props.away ? (
<div className="h-screen dark:bg-neutral-900">
<main className="mx-auto max-w-3xl px-4 py-24">
@ -57,7 +56,7 @@ export default function Book(props: BookPageProps) {
</main>
</div>
) : (
<BookingPage {...props} locationLabels={locationLabels} />
<BookingPage {...props} />
);
}
@ -138,8 +137,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
};
const eventTypeObject = [eventType].map((e) => {
let locations = eventTypeRaw.locations || [];
locations = privacyFilteredLocations(locations as LocationObject[]);
return {
...e,
locations: locations,
periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null,
schedulingType: null,

View File

@ -5,9 +5,11 @@ import { RRule } from "rrule";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import { getLocationValueForDB, LocationObject } from "@calcom/app-store/locations";
import { handlePayment } from "@calcom/app-store/stripepayment/lib/server";
import { cancelScheduledJobs, scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import EventManager from "@calcom/core/EventManager";
import { getEventName } from "@calcom/core/event";
import { getUserAvailability } from "@calcom/core/getUserAvailability";
import dayjs from "@calcom/dayjs";
import {
@ -34,7 +36,6 @@ import type { EventResult, PartialReference } from "@calcom/types/EventManager";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
import { getEventName } from "@lib/event";
import sendPayload from "@lib/webhooks/sendPayload";
import getSubscribers from "@lib/webhooks/subscriptions";
@ -174,6 +175,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
return {
...eventType,
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: (eventType.locations ?? []) as LocationObject[],
};
};
@ -358,9 +360,8 @@ async function handler(req: NextApiRequest) {
const seed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}:${new Date().getTime()}`;
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
const location = !!eventType.locations ? (eventType.locations as Array<{ type: string }>)[0] : "";
const locationType = !!location && location.type ? location.type : "";
const bookingLocation = getLocationValueForDB(reqBody.location, eventType.locations);
console.log(bookingLocation, reqBody.location, eventType.locations);
const customInputs = {} as NonNullable<CalendarEvent["customInputs"]>;
const teamMemberPromises =
@ -387,7 +388,7 @@ async function handler(req: NextApiRequest) {
eventType: eventType.title,
eventName: eventType.eventName,
host: organizerUser.name || "Nameless",
location: locationType,
location: bookingLocation,
t: tOrganizer,
};
@ -408,7 +409,7 @@ async function handler(req: NextApiRequest) {
language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" },
},
attendees: attendeesList,
location: reqBody.location, // Will be processed by the EventManager later.
location: bookingLocation, // Will be processed by the EventManager later.
/** For team events & dynamic collective events, we will need to handle each member destinationCalendar eventually */
destinationCalendar: eventType.destinationCalendar || organizerUser.destinationCalendar,
hideCalendarNotes: eventType.hideCalendarNotes,

View File

@ -11,6 +11,7 @@ import z from "zod";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { DailyLocationType } from "@calcom/app-store/locations";
import { refund } from "@calcom/app-store/stripepayment/lib/server";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { deleteMeeting } from "@calcom/core/videoClient";
@ -238,7 +239,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}
/** TODO: Remove this without breaking functionality */
if (bookingToDelete.location === "integrations:daily") {
if (bookingToDelete.location === DailyLocationType) {
bookingToDelete.user.credentials.push(FAKE_DAILY_CREDENTIAL);
}

View File

@ -70,7 +70,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
isGlobal={data.isGlobal}
type={data.type}
logo={data.logo}
categories={[data.category]}
categories={data.categories ?? [data.category]}
author={data.publisher}
feeType={data.feeType || "usage-based"}
price={data.price || 0}

View File

@ -2,13 +2,13 @@ import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { z } from "zod";
import { privacyFilteredLocations, LocationObject } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib";
import { availiblityPageEventTypeSelect } from "@calcom/prisma";
import prisma from "@calcom/prisma";
import { getWorkingHours } from "@lib/availability";
import { GetBookingType } from "@lib/getBooking";
import { locationHiddenFilter, LocationObject } from "@lib/location";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
@ -105,7 +105,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
periodStartDate: hashedLink.eventType.periodStartDate?.toString() ?? null,
periodEndDate: hashedLink.eventType.periodEndDate?.toString() ?? null,
slug,
locations: locationHiddenFilter(locations),
locations: privacyFilteredLocations(locations),
users: users.map((u) => ({
name: u.name,
username: u.username,

View File

@ -1,7 +1,6 @@
import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { getLocationLabels } from "@calcom/app-store/utils";
import { parseRecurringEvent } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
@ -17,10 +16,7 @@ import { ssrInit } from "@server/lib/ssr";
export type HashLinkPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: HashLinkPageProps) {
const { t } = useLocale();
const locationLabels = getLocationLabels(t);
return <BookingPage {...props} locationLabels={locationLabels} />;
return <BookingPage {...props} />;
}
Book.isThemeSupported = true;

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,4 @@
import { Prisma } from "@prisma/client";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import classNames from "classnames";
import { createEvent } from "ics";
@ -9,6 +10,8 @@ import { useEffect, useRef, useState } from "react";
import { RRule } from "rrule";
import { z } from "zod";
import { getEventLocationValue, getSuccessPageLocationMessage } from "@calcom/app-store/locations";
import { getEventName } from "@calcom/core/event";
import dayjs from "@calcom/dayjs";
import {
sdkActionManager,
@ -26,13 +29,11 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
import { isBrowserLocale24h } from "@calcom/lib/timeFormat";
import { localStorage } from "@calcom/lib/webstorage";
import prisma from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import Button from "@calcom/ui/Button";
import { Icon } from "@calcom/ui/Icon";
import { EmailInput } from "@calcom/ui/form/fields";
import { asStringOrThrow } from "@lib/asStringOrNull";
import { getEventName } from "@lib/event";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import { isSuccessRedirectAvailable } from "@lib/isSuccessRedirectAvailable";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@ -140,7 +141,15 @@ export default function Success(props: SuccessProps) {
const { t } = useLocale();
const router = useRouter();
const { location: _location, name, reschedule, listingStatus, status, isSuccessBookingPage } = router.query;
const location = Array.isArray(_location) ? _location[0] : _location;
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(_location)
? _location[0] || ""
: _location || "";
if (!location) {
// Can't use logger.error because it throws error on client. stdout isn't available to it.
console.error(`No location found `);
}
const [is24h, setIs24h] = useState(isBrowserLocale24h());
const { data: session } = useSession();
@ -155,16 +164,12 @@ export default function Success(props: SuccessProps) {
const attendeeName = typeof name === "string" ? name : "Nameless";
const locationFromEventType = !!eventType.locations
? (eventType.locations as Array<{ type: string }>)[0]
: "";
const locationType = !!locationFromEventType ? locationFromEventType.type : "";
const eventNameObject = {
attendeeName,
eventType: props.eventType.title,
eventName: (props.dynamicEventName as string) || props.eventType.eventName,
host: props.profile.name || "Nameless",
location: locationType,
location: location,
t,
};
const metadata = props.eventType?.metadata as { giphyThankYouPage: string };
@ -252,6 +257,9 @@ export default function Success(props: SuccessProps) {
`booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
);
const customInputs = bookingInfo?.customInputs;
const locationToDisplay = getSuccessPageLocationMessage(location, t);
return (
<div className={isEmbed ? "" : "h-screen bg-neutral-100 dark:bg-neutral-900"} data-testid="success-page">
{userIsOwner && !isEmbed && (
@ -355,16 +363,16 @@ export default function Success(props: SuccessProps) {
</div>
</>
)}
{location && (
{locationToDisplay && (
<>
<div className="mt-3 font-medium">{t("where")}</div>
<div className="col-span-2 mt-3">
{location.startsWith("http") ? (
<a title="Meeting Link" href={location}>
{location}
{locationToDisplay.startsWith("http") ? (
<a title="Meeting Link" href={locationToDisplay}>
{locationToDisplay}
</a>
) : (
location
locationToDisplay
)}
</div>
</>

View File

@ -2,13 +2,13 @@ import { UserPlan } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { privacyFilteredLocations, LocationObject } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib";
import prisma from "@calcom/prisma";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getWorkingHours } from "@lib/availability";
import getBooking, { GetBookingType } from "@lib/getBooking";
import { locationHiddenFilter, LocationObject } from "@lib/location";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
@ -116,13 +116,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
eventType.schedule = null;
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
console.log("locations", locations);
const eventTypeObject = Object.assign({}, eventType, {
metadata: (eventType.metadata || {}) as JSONObject,
periodStartDate: eventType.periodStartDate?.toString() ?? null,
periodEndDate: eventType.periodEndDate?.toString() ?? null,
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: locationHiddenFilter(locations),
locations: privacyFilteredLocations(locations),
users: eventType.users.map((user) => ({
name: user.name,
username: user.username,

View File

@ -1,7 +1,7 @@
import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { getLocationLabels } from "@calcom/app-store/utils";
import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations";
import { parseRecurringEvent } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
@ -16,9 +16,8 @@ export type TeamBookingPageProps = inferSSRProps<typeof getServerSideProps>;
export default function TeamBookingPage(props: TeamBookingPageProps) {
const { t } = useLocale();
const locationLabels = getLocationLabels(t);
return <BookingPage {...props} locationLabels={locationLabels} />;
return <BookingPage {...props} />;
}
TeamBookingPage.isThemeSupported = true;
@ -88,6 +87,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const eventType = {
...eventTypeRaw,
//TODO: Use zodSchema to verify it instead of using Type Assertion
locations: privacyFilteredLocations(eventTypeRaw.locations as LocationObject[]),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
};

View File

@ -8,6 +8,7 @@ import { z } from "zod";
import { StripeData } from "@calcom/app-store/stripepayment/lib/server";
import getApps, { getLocationOptions } from "@calcom/app-store/utils";
import { LocationObject, EventLocationType } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -20,7 +21,6 @@ import { Button, showToast } from "@calcom/ui/v2";
import { asStringOrThrow } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
import { LocationObject, LocationType } from "@lib/location";
import { inferSSRProps } from "@lib/types/inferSSRProps";
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings
@ -52,7 +52,7 @@ export type FormValues = {
hideCalendarNotes: boolean;
hashedLink: string | undefined;
locations: {
type: LocationType;
type: EventLocationType["type"];
address?: string;
link?: string;
hostPhoneNumber?: string;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>

After

Width:  |  Height:  |  Size: 409 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-map-pin"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>

After

Width:  |  Height:  |  Size: 520 B

View File

@ -453,7 +453,7 @@
"booking_reschedule_confirmation": "Reschedule your {{eventTypeTitle}} with {{profileName}}",
"in_person_meeting": "In-person meeting",
"link_meeting": "Link meeting",
"phone_call": "Phone call",
"phone_call": "Attendee Phone Number",
"your_number": "Your phone number",
"phone_number": "Phone Number",
"attendee_phone_number": "Attendee Phone Number",

View File

@ -21,10 +21,7 @@ If we rename all existing apps to their slug names, we can remove type and then
- Edit command Improvements
- Prefill fields in edit command -> It allows only that content to change which user wants to change.
- Don't override icon.svg
- For Video Apps
- Update app-store/locations.ts
- \_metadata.ts should have locationType and locationLabel props.
- Merge app-store:watch and app-store commands, introduce app-store --watch
- Merge app-store:watch and app-store commands; introduce app-store --watch
- Allow inputs in non interactive way as well - That would allow easily copy pasting commands.
- An app created through CLI should be able to completely skip API validation for testing purposes. Credentials should be created with no API specified specific to the app. It would allow us to test any app end to end not worrying about the corresponding API endpoint.
- Require assets path relative to app dir.

View File

@ -46,6 +46,7 @@ const updatePackageJson = ({ slug, appDescription, appDirPath }) => {
const BaseAppFork = {
create: function* ({
category,
subCategory,
editMode = false,
appDescription,
appName,
@ -66,6 +67,31 @@ const BaseAppFork = {
video: "conferencing",
};
const dataFromCategory =
category === "video"
? {
appData: {
location: {
type: `integrations:${slug}_video`,
label: `${appName}`,
},
},
}
: {};
const dataFromSubCategory =
category === "video" && subCategory === "static"
? {
appData: {
...dataFromCategory.appData,
location: {
...dataFromCategory.appData.location,
linkType: "static",
organizerInputPlaceholder: "https://anything.anything",
urlRegExp: "",
},
},
}
: {};
let config = {
"/*": "Don't modify slug - If required, do it using cli edit command",
name: appName,
@ -82,6 +108,8 @@ const BaseAppFork = {
description: appDescription,
// TODO: Use this to avoid edit and delete on the apps created outside of cli
__createdUsingCli: true,
...dataFromCategory,
...dataFromSubCategory,
};
const currentConfig = JSON.parse(fs.readFileSync(`${appDirPath}/config.json`).toString());
config = {
@ -93,6 +121,10 @@ const BaseAppFork = {
`${appDirPath}/README.mdx`,
fs.readFileSync(`${appDirPath}/README.mdx`).toString().replace("_DESCRIPTION_", appDescription)
);
fs.writeFileSync(
`${appDirPath}/README.mdx`,
fs.readFileSync(`${appDirPath}/README.mdx`).toString().replace("_DESCRIPTION_", appDescription)
);
message = !editMode ? "Forked base app" : "Updated app";
yield message;
},
@ -122,6 +154,10 @@ const Seed = {
});
}
// Add the message as a property to first item so that it stays always at the top
seedConfig[0]["/*"] =
"This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually";
// Add the message as a property to first item so that it stays always at the top
seedConfig[0]["/*"] =
"This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually";
@ -151,41 +187,65 @@ const CreateApp = ({ noDbUpdate, slug = null, editMode = false }) => {
const [appInputData, setAppInputData] = useState({});
const [inputIndex, setInputIndex] = useState(0);
const fields = [
{ label: "App Title", name: "appName", type: "text" },
{ label: "App Description", name: "appDescription", type: "text" },
{ label: "App Title", name: "appName", type: "text", explainer: "Keep it very short" },
{
label: "App Description",
name: "appDescription",
type: "text",
explainer:
"A detailed description of your app. You can later modify README.mdx to add slider and other components",
},
{
label: "Category of App",
name: "appCategory",
type: "select",
options: [
{ label: "calendar", value: "calendar" },
{ label: "video", value: "video" },
{ label: "payment", value: "payment" },
{ label: "messaging", value: "messaging" },
{ label: "web3", value: "web3" },
{ label: "other", value: "other" },
{ label: "Calendar", value: "calendar" },
{
label: "Static Link - Video",
value: "video_static",
explainer:
"Apps like Ping.gg/Riverside/Whereby which require you to provide a link to join your room",
},
{ label: "Other - Video", value: "video_other" },
{ label: "Payment", value: "payment" },
{ label: "Messaging", value: "messaging" },
{ label: "Web3", value: "web3" },
{ label: "Other", value: "other" },
],
explainer: "This is how apps are categorized in App Store.",
},
{ label: "Publisher Name", name: "publisherName", type: "text", explainer: "Let users know who you are" },
{
label: "Publisher Email",
name: "publisherEmail",
type: "text",
explainer: "Let users know how they can contact you.",
},
{ label: "Publisher Name", name: "publisherName", type: "text" },
{ label: "Publisher Email", name: "publisherEmail", type: "text" },
];
const field = fields[inputIndex];
const fieldLabel = field?.label || "";
const fieldName = field?.name || "";
const fieldValue = appInputData[fieldName] || "";
const appName = appInputData["appName"];
const category = appInputData["appCategory"];
const rawCategory = appInputData["appCategory"] || "";
const appDescription = appInputData["appDescription"];
const publisherName = appInputData["publisherName"];
const publisherEmail = appInputData["publisherEmail"];
const [status, setStatus] = useState<"inProgress" | "done">("inProgress");
const allFieldsFilled = inputIndex === fields.length;
const [progressUpdate, setProgressUpdate] = useState("");
const category = rawCategory.split("_")[0];
const subCategory = rawCategory.split("_")[1];
if (!editMode) {
slug = getSlugFromAppName(appName);
}
useEffect(() => {
// When all fields have been filled
if (allFieldsFilled) {
const it = BaseAppFork.create({
category,
subCategory,
appDescription,
appName,
slug,
@ -210,10 +270,6 @@ const CreateApp = ({ noDbUpdate, slug = null, editMode = false }) => {
return <Text>--slug is required</Text>;
}
if (!editMode) {
slug = getSlugFromAppName(appName);
}
if (allFieldsFilled) {
return (
<Box flexDirection="column">
@ -226,8 +282,9 @@ const CreateApp = ({ noDbUpdate, slug = null, editMode = false }) => {
{status === "done" ? (
<Box flexDirection="column" paddingTop={2} paddingBottom={2}>
<Text bold italic>
Just wait for few seconds to process to exit and you are good to go. Your App code exists at $
{getAppDirPath(slug)}
Just wait for a few seconds for process to exit and then you are good to go. Your App code
exists at ${getAppDirPath(slug)}
Tip: Go and change the logo of your app by replacing {getAppDirPath(slug) + "/static/icon.svg"}
</Text>
<Text bold italic>
App Summary:
@ -284,49 +341,51 @@ const CreateApp = ({ noDbUpdate, slug = null, editMode = false }) => {
}
return (
<Box flexDirection="column">
<Box>
<Text color="green">{`${fieldLabel}:`}</Text>
{field.type == "text" ? (
<TextInput
value={fieldValue}
onSubmit={(value) => {
if (!value) {
return;
}
setInputIndex((index) => {
return index + 1;
});
}}
onChange={(value) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: value,
};
});
}}
/>
) : (
<SelectInput<string>
items={field.options}
onSelect={(item) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: item.value,
};
});
setInputIndex((index) => {
return index + 1;
});
}}
/>
)}
</Box>
<Box>
<Text color="gray" italic>
{field.explainer}
</Text>
<Box flexDirection="column">
<Box>
<Text color="green">{`${fieldLabel}:`}</Text>
{field.type == "text" ? (
<TextInput
value={fieldValue}
onSubmit={(value) => {
if (!value) {
return;
}
setInputIndex((index) => {
return index + 1;
});
}}
onChange={(value) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: value,
};
});
}}
/>
) : (
<SelectInput<string>
items={field.options}
onSelect={(item) => {
setAppInputData((appInputData) => {
return {
...appInputData,
[fieldName]: item.value,
};
});
setInputIndex((index) => {
return index + 1;
});
}}
/>
)}
</Box>
<Box>
<Text color="gray" italic>
{field.explainer}
</Text>
</Box>
</Box>
</Box>
);

View File

@ -133,6 +133,7 @@ function generateFiles() {
` ${app.name}: dynamic(() =>import("./${app.path}/components/InstallAppButton")),`,
})
);
const banner = `/**
This file is autogenerated using the command \`yarn app-store:build --watch\`.
Don't modify this file manually.

View File

@ -32,8 +32,15 @@ export async function getAppRegistry() {
// Skip if app isn't installed
/* This is now handled from the DB */
// if (!app.installed) return apps;
const { rating, reviews, trending, verified, ...remainingAppProps } = app;
apps.push({
...app,
rating: rating || 0,
reviews: reviews || 0,
trending: trending || true,
verified: verified || true,
...remainingAppProps,
category: app.category || "other",
installed:
true /* All apps from DB are considered installed by default. @TODO: Add and filter our by `enabled` property */,
});

View File

@ -1,16 +1,10 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
// FIXME: Currently for an app to be shown as installed, it must have this variable set. Either hardcoded or if it depends on some env variable, that should be checked here
installed: true,
rating: 0,
reviews: 0,
trending: true,
verified: true,
...config,
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -21,6 +21,6 @@ export const metadata = {
variant: "conferencing",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -20,6 +20,6 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,19 +1,19 @@
import { LocationType } from "@calcom/core/location";
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
// FIXME: Currently for an app to be shown as installed, it must have this variable set. Either hardcoded or if it depends on some env variable, that should be checked here
installed: true,
rating: 0,
reviews: 0,
trending: true,
verified: true,
locationType: LocationType.Around,
locationLabel: "Around Video",
appData: {
location: {
linkType: "static",
type: "integrations:around_video",
label: "Around Video",
urlRegExp: "^http(s)?:\\/\\/(www\\.)?around.co\\/[a-zA-Z0-9]*",
organizerInputPlaceholder: "https://www.around.co/rick",
},
},
...config,
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -20,6 +20,6 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,6 +1,5 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
@ -22,9 +21,14 @@ export const metadata = {
title: "Cal Video",
isGlobal: true,
email: "help@cal.com",
locationType: LocationType.Daily,
locationLabel: "Cal Video",
appData: {
location: {
linkType: "dynamic",
type: "integrations:daily",
label: "Cal Video",
},
},
key: { apikey: process.env.DAILY_API_KEY },
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
@ -13,6 +13,6 @@ export const metadata = {
licenseRequired: true,
isProOnly: true,
...config,
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -21,6 +21,6 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -21,6 +21,6 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -21,6 +21,6 @@ export const metadata = {
variant: "other",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,7 +1,6 @@
import { validJson } from "@calcom/lib/jsonUtils";
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
@ -22,6 +21,6 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,7 +1,6 @@
import { validJson } from "@calcom/lib/jsonUtils";
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
@ -23,8 +22,13 @@ export const metadata = {
verified: true,
isGlobal: true,
email: "help@cal.com",
locationType: LocationType.GoogleMeet,
locationLabel: "Google Meet",
} as App;
appData: {
location: {
linkType: "dynamic",
type: "integrations:google:meet",
label: "Google Meet",
},
},
} as AppMeta;
export default metadata;

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -21,6 +21,6 @@ export const metadata = {
title: "HubSpot CRM",
trending: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,7 +1,6 @@
import { randomString } from "@calcom/lib/random";
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
@ -23,9 +22,14 @@ export const metadata = {
trending: true,
isGlobal: false,
email: "support@huddle01.com",
locationType: LocationType.Huddle01,
locationLabel: "Huddle01 Video",
appData: {
location: {
linkType: "dynamic",
type: "integrations:huddle01",
label: "Huddle01 Video",
},
},
key: { apikey: randomString(12) },
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,6 +1,5 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
@ -22,8 +21,13 @@ export const metadata = {
trending: true,
isGlobal: false,
email: "help@cal.com",
locationType: LocationType.Jitsi,
locationLabel: "Jitsi Video",
} as App;
appData: {
location: {
linkType: "dynamic",
type: "integrations:jitsi",
label: "Jitsi Video",
},
},
} as AppMeta;
export default metadata;

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -20,6 +20,6 @@ export const metadata = {
url: "https://larksuite.com/",
verified: true,
email: "alan@larksuite.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,51 +1,197 @@
import { BookingStatus } from "@prisma/client";
import type { TFunction } from "next-i18next";
export enum DefaultLocationType {
import logger from "@calcom/lib/logger";
import { Ensure, Optional } from "@calcom/types/utils";
import type { EventLocationTypeFromAppMeta } from "../types/App";
import { appStoreMetadata } from "./apps.browser.generated";
export type DefaultEventLocationType = {
default: true;
type: DefaultEventLocationTypeEnum;
label: string;
messageForOrganizer: string;
iconUrl: string;
// HACK: `variable` and `defaultValueVariable` are required due to legacy reason where different locations were stored in different places.
variable: "locationType" | "locationAddress" | "locationLink" | "locationPhoneNumber" | "phone";
defaultValueVariable: "address" | "link" | "hostPhoneNumber" | "phone";
} & (
| {
organizerInputType: "phone" | "text" | null;
organizerInputPlaceholder?: string | null;
attendeeInputType?: null;
attendeeInputPlaceholder?: null;
}
| {
attendeeInputType: "phone" | "text" | null;
attendeeInputPlaceholder: string;
organizerInputType?: null;
organizerInputPlaceholder?: null;
}
);
type EventLocationTypeFromApp = Ensure<EventLocationTypeFromAppMeta, "defaultValueVariable" | "variable">;
export type EventLocationType = DefaultEventLocationType | EventLocationTypeFromApp;
export const DailyLocationType = "integrations:daily";
export enum DefaultEventLocationTypeEnum {
InPerson = "inPerson",
/**
* Booker Phone
*/
Phone = "phone",
/**
* Organizer Phone
*/
UserPhone = "userPhone",
Link = "link",
}
/** If your App has a location option, add it here */
export enum AppStoreLocationType {
GoogleMeet = "integrations:google:meet",
Zoom = "integrations:zoom",
Daily = "integrations:daily",
Jitsi = "integrations:jitsi",
Huddle01 = "integrations:huddle01",
Tandem = "integrations:tandem",
Teams = "integrations:office365_video",
Whereby = "integrations:whereby_video",
Around = "integrations:around_video",
Riverside = "integrations:riverside_video",
Ping = "integrations:ping_video",
}
export const defaultLocations: DefaultEventLocationType[] = [
{
default: true,
type: DefaultEventLocationTypeEnum.InPerson,
label: "In Person",
organizerInputType: "text",
messageForOrganizer: "Provide an Address or Place",
// HACK:
variable: "locationAddress",
defaultValueVariable: "address",
iconUrl: "/map-pin.svg",
},
{
default: true,
type: DefaultEventLocationTypeEnum.Link,
label: "Link",
organizerInputType: "text",
variable: "locationLink",
messageForOrganizer: "Provide a Meeting Link",
defaultValueVariable: "link",
iconUrl: "/globe.svg",
},
{
default: true,
type: DefaultEventLocationTypeEnum.Phone,
label: "Attendee Phone Number",
variable: "phone",
organizerInputType: null,
attendeeInputType: "phone",
attendeeInputPlaceholder: `enter_phone_number`,
defaultValueVariable: "phone",
messageForOrganizer: "Cal will ask your invitee to enter a phone number before scheduling.",
// This isn't inputType phone because organizer doesn't need to provide it.
// inputType: "phone"
iconUrl: "/phone.svg",
},
{
default: true,
type: DefaultEventLocationTypeEnum.UserPhone,
label: "Organizer Phone Number",
messageForOrganizer: "Provide your phone number",
organizerInputType: "phone",
variable: "locationPhoneNumber",
defaultValueVariable: "hostPhoneNumber",
iconUrl: "/phone.svg",
},
];
export type LocationObject = {
type: LocationType;
address?: string;
link?: string;
type: string;
displayLocationPublicly?: boolean;
hostPhoneNumber?: string;
} & Partial<Record<"address" | "link" | "hostPhoneNumber" | "phone", string>>;
// integrations:jitsi | 919999999999 | Delhi | https://manual.meeting.link | Around Video
export type BookingLocationValue = string;
export const AppStoreLocationType: Record<string, string> = {};
const locationsFromApps: EventLocationTypeFromApp[] = [];
for (const [appName, meta] of Object.entries(appStoreMetadata)) {
const location = meta.appData?.location;
if (location) {
const newLocation = {
...location,
messageForOrganizer: location.messageForOrganizer || `Set ${location.label} link`,
iconUrl: meta.logo,
// For All event location apps, locationLink is where we store the input
// TODO: locationLink and link seems redundant. We can modify the code to keep just one of them.
variable: location.variable || "locationLink",
defaultValueVariable: location.defaultValueVariable || "link",
};
// Static links always require organizer to input
if (newLocation.linkType === "static") {
newLocation.organizerInputType = location.organizerInputType || "text";
if (newLocation.organizerInputPlaceholder?.match(/https?:\/\//)) {
// HACK: Translation ends up removing https? if it's in the beginning :(
newLocation.organizerInputPlaceholder = ` ${newLocation.organizerInputPlaceholder}`;
}
} else {
newLocation.organizerInputType = null;
}
AppStoreLocationType[appName] = newLocation.type;
locationsFromApps.push({
...newLocation,
});
}
}
const locationsTypes = [...defaultLocations, ...locationsFromApps];
export const getStaticLinkBasedLocation = (locationType: string) =>
locationsFromApps.find((l) => l.linkType === "static" && l.type === locationType);
export const getEventLocationTypeFromApp = (locationType: string) =>
locationsFromApps.find((l) => l.type === locationType);
export const getEventLocationType = (locationType: string | undefined | null) =>
locationsTypes.find((l) => l.type === locationType);
export const getEventLocationTypeFromValue = (value: string | undefined | null) => {
if (!value) {
return null;
}
return locationsTypes.find((l) => {
if (l.default || l.linkType == "dynamic" || !l.urlRegExp) {
return;
}
return new RegExp(l.urlRegExp).test(value);
});
};
export const LocationType = { ...DefaultLocationType, ...AppStoreLocationType };
export type LocationType = DefaultLocationType | AppStoreLocationType;
export const guessEventLocationType = (locationTypeOrValue: string | undefined | null) =>
getEventLocationType(locationTypeOrValue) || getEventLocationTypeFromValue(locationTypeOrValue);
export const locationHiddenFilter = (locations: LocationObject[]) =>
locations.filter((el) => {
export const LocationType = { ...DefaultEventLocationTypeEnum, ...AppStoreLocationType };
type PrivacyFilteredLocationObject = Optional<LocationObject, "address" | "link">;
export const privacyFilteredLocations = (locations: LocationObject[]): PrivacyFilteredLocationObject[] => {
const locationsAfterPrivacyFilter = locations.map((location) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
logger.debug(`Couldn't find location type. App might be uninstalled: ${location.type} `);
}
// Filter out locations that are not to be displayed publicly
const values = Object.values(AppStoreLocationType);
// Display if the location can be set to public - and also display all locations like google meet etc
if (el.displayLocationPublicly || values.includes(el["type"] as unknown as AppStoreLocationType))
return el;
else {
delete el.address;
delete el.link;
return el;
if (location.displayLocationPublicly || !eventLocationType) {
return location;
} else {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { address: _1, link: _2, hostPhoneNumber: _3, ...privacyFilteredLocation } = location;
logger.debug("Applied Privacy Filter", location, privacyFilteredLocation);
return privacyFilteredLocation;
}
});
return locationsAfterPrivacyFilter;
};
/**
* Use this function for translating event location to a readable string
@ -53,29 +199,131 @@ export const locationHiddenFilter = (locations: LocationObject[]) =>
* @param t
* @returns string
*/
export const LocationOptionsToString = (location: string, t: TFunction) => {
switch (location) {
case LocationType.InPerson:
return t("set_address_place");
case LocationType.Link:
return t("set_link_meeting");
case LocationType.Phone:
return t("cal_invitee_phone_number_scheduling");
case LocationType.GoogleMeet:
return t("cal_provide_google_meet_location");
case LocationType.Zoom:
return t("cal_provide_zoom_meeting_url");
case LocationType.Daily:
return t("cal_provide_video_meeting_url");
case LocationType.Jitsi:
return t("cal_provide_jitsi_meeting_url");
case LocationType.Huddle01:
return t("cal_provide_huddle01_meeting_url");
case LocationType.Tandem:
return t("cal_provide_tandem_meeting_url");
case LocationType.Teams:
return t("cal_provide_teams_meeting_url");
default:
return null;
export const getMessageForOrganizer = (location: string, t: TFunction) => {
const videoLocation = getEventLocationTypeFromApp(location);
const defaultLocation = defaultLocations.find((l) => l.type === location);
if (defaultLocation) {
return t(defaultLocation.messageForOrganizer);
}
if (videoLocation && videoLocation.linkType !== "static") {
return t(`Cal will provide a ${videoLocation.label} URL.`);
}
return "";
};
/**
* Use this function to translate booking location value to a readable string
* @param linkValue
* @param translationFunction
* @returns
*/
export const getHumanReadableLocationValue = (
linkValue: string | undefined | null,
translationFunction: TFunction
): string => {
if (!linkValue) {
return translationFunction("no_location");
}
// Just in case linkValue is a `locationType.type`(for old bookings)
const eventLocationType = getEventLocationType(linkValue);
if (eventLocationType) {
// If we can find a video location based on linkValue then it means that the linkValue is something like integrations:google-meet and in that case we don't have the meeting URL to show.
// Show a generic message in that case.
return `${eventLocationType.label}`;
}
// Otherwise just show the available link value which can be a Phone number, a URL or a physical address of a place.
return linkValue || "";
};
export const locationKeyToString = (location: LocationObject) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
return null;
}
const defaultValueVariable = eventLocationType.defaultValueVariable;
if (!defaultValueVariable) {
console.error(`defaultValueVariable not set for ${location.type}`);
return "";
}
return location[defaultValueVariable] || eventLocationType.label;
};
export const getEventLocationWithType = (
locations: LocationObject[],
locationType: EventLocationType["type"] | undefined
) => {
const location = locations.find((location) => location.type === locationType);
return location;
};
// FIXME: It assumes that type would be sent mostly now. If just in case a value and not type is sent(when old frontend sends requests to new backend), below forEach won't be able to find a match and thus bookingLocation would still be correct equal to reqBody.location
// We must handle the situation where frontend doesn't send us the value because it doesn't have it(displayLocationPublicly not set)
// But we want to store the actual location(except dynamic URL based location type) so that Emails, Calendars pick the value only.
// TODO: We must store both type as well as value so that we know the type of data that we are having. Is it an address or a phone number? This is to be done post v2.0
export const getLocationValueForDB = (
bookingLocationTypeOrValue: EventLocationType["type"],
eventLocations: LocationObject[]
) => {
let bookingLocation = bookingLocationTypeOrValue;
eventLocations.forEach((location) => {
if (location.type === bookingLocationTypeOrValue) {
const eventLocationType = getEventLocationType(bookingLocationTypeOrValue);
if (!eventLocationType) {
return;
}
if (!eventLocationType.default && eventLocationType.linkType === "dynamic") {
// Dynamic link based locations should still be saved as type. The beyond logic generates meeting URL based on the type.
// This difference can be avoided when we start storing both type and value of a location
return;
}
bookingLocation = location[eventLocationType.defaultValueVariable] || bookingLocation;
}
});
return bookingLocation;
};
export const getEventLocationValue = (eventLocations: LocationObject[], bookingLocation: LocationObject) => {
const eventLocationType = getEventLocationType(bookingLocation?.type);
if (!eventLocationType) {
return "";
}
const defaultValueVariable = eventLocationType.defaultValueVariable;
if (!defaultValueVariable) {
console.error(`${defaultValueVariable} not set for ${bookingLocation.type}`);
return "";
}
const eventLocation = getEventLocationWithType(eventLocations, bookingLocation?.type);
if (!eventLocation) {
console.error(`Could not find eventLocation for ${bookingLocation}`);
return "";
}
// Must send .type here if value isn't available due to privacy setting.
// For Booker Phone Number, it would be a value always. For others, value is either autogenerated or provided by Organizer and thus it's possible that organizer doesn't want it to show
// Backend checks for `integration` to generate link
// TODO: use zodSchema to ensure the type of data is correct
return (
bookingLocation[defaultValueVariable] || eventLocation[defaultValueVariable] || eventLocationType.type
);
};
export function getSuccessPageLocationMessage(location: EventLocationType["type"], t: TFunction) {
const eventLocationType = getEventLocationType(location);
let locationToDisplay = location;
if (eventLocationType && !eventLocationType.default && eventLocationType.linkType === "dynamic") {
const isConfirmed = status === BookingStatus.ACCEPTED;
if (status === BookingStatus.CANCELLED || status === BookingStatus.REJECTED) {
locationToDisplay == t("web_conference");
} else if (isConfirmed) {
locationToDisplay =
getHumanReadableLocationValue(location, t) + ": " + t("meeting_url_in_conformation_email");
} else {
locationToDisplay = t("web_conferencing_details_to_follow");
}
}
return locationToDisplay;
}

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -21,6 +21,6 @@ export const metadata = {
variant: "other",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -19,6 +19,6 @@ export const metadata = {
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,6 +1,5 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
@ -20,8 +19,13 @@ export const metadata = {
title: "MS Teams (Requires work/school account)",
trending: true,
email: "help@cal.com",
locationType: LocationType.Teams,
locationLabel: "MS Teams",
} as App;
appData: {
location: {
linkType: "dynamic",
type: "integrations:office365_video",
label: "MS Teams",
},
},
} as AppMeta;
export default metadata;

View File

@ -1,5 +1,4 @@
import { LocationType } from "@calcom/core/location";
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
@ -11,9 +10,16 @@ export const metadata = {
reviews: 0,
trending: true,
verified: true,
locationType: LocationType.Ping,
locationLabel: "Ping.gg",
appData: {
location: {
linkType: "static",
type: "integrations:ping_video",
label: "Ping.gg",
organizerInputPlaceholder: "https://www.ping.gg/call/theo",
urlRegExp: "^http(s)?:\\/\\/(www\\.)?ping.gg\\/call\\/[a-zA-Z0-9]*",
},
},
...config,
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,5 +1,4 @@
import { LocationType } from "@calcom/core/location";
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
@ -11,9 +10,16 @@ export const metadata = {
reviews: 0,
trending: true,
verified: true,
locationType: LocationType.Riverside,
locationLabel: "Riverside Video",
locationPlaceholder: "https://www.riverside.fm/studio/rick",
appData: {
location: {
label: "Riverside Video",
urlRegExp: "^http(s)?:\\/\\/(www\\.)?riverside.fm\\/studio\\/[a-zA-Z0-9]*",
type: "integrations:riverside_video",
linkType: "static",
},
},
...config,
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -20,6 +20,6 @@ export const metadata = {
variant: "conferencing",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -2,6 +2,7 @@ import { WebClient } from "@slack/web-api";
import { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { DailyLocationType } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs";
import { WEBAPP_URL } from "@calcom/lib/constants";
import db from "@calcom/prisma";
@ -94,7 +95,7 @@ export default async function createEvent(req: NextApiRequest, res: NextApiRespo
email: foundUser?.email ?? "",
name: foundUser?.username ?? "",
guests: await Promise.all(invitedGuestsEmails),
location: "integrations:daily", // Defaulting to daily video to make this a bit more usefull than in-person
location: DailyLocationType, // Defaulting to daily video to make this a bit more usefull than in-person
timeZone: foundUser?.timeZone ?? "",
language: foundUser?.locale ?? "en",
customInputs: [{ label: "", value: "" }],

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -25,6 +25,6 @@ export const metadata = {
variant: "payment",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,6 +1,5 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
@ -21,8 +20,13 @@ export const metadata = {
reviews: 0,
isGlobal: false,
email: "help@cal.com",
locationType: LocationType.Tandem,
locationLabel: "Tandem Video",
} as App;
appData: {
location: {
linkType: "dynamic",
type: "integrations:tandem",
label: "Tandem Video",
},
},
} as AppMeta;
export default metadata;

View File

@ -1,16 +1,10 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
export const metadata = {
category: "other",
// FIXME: Currently for an app to be shown as installed, it must have this variable set. Either hardcoded or if it depends on some env variable, that should be checked here
installed: true,
rating: 0,
reviews: 0,
trending: true,
verified: true,
...config,
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,17 +1,17 @@
import { Prisma } from "@prisma/client";
import { TFunction } from "next-i18next";
import type { App } from "@calcom/types/App";
import { defaultLocations, EventLocationType, LocationType } from "@calcom/app-store/locations";
import type { App, AppMeta } from "@calcom/types/App";
// If you import this file on any app it should produce circular dependency
// import appStore from "./index";
import { appStoreMetadata } from "./apps.browser.generated";
import { LocationType } from "./locations";
const ALL_APPS_MAP = Object.keys(appStoreMetadata).reduce((store, key) => {
store[key] = appStoreMetadata[key as keyof typeof appStoreMetadata];
return store;
}, {} as Record<string, App>);
}, {} as Record<string, AppMeta>);
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
select: { id: true, type: true, key: true, userId: true, appId: true },
@ -23,7 +23,7 @@ export const ALL_APPS = Object.values(ALL_APPS_MAP);
type OptionTypeBase = {
label: string;
value: LocationType;
value: EventLocationType["type"];
disabled?: boolean;
};
@ -33,15 +33,15 @@ function translateLocations(locations: OptionTypeBase[], t: TFunction) {
label: t(l.label),
}));
}
const defaultLocations: OptionTypeBase[] = [
{ value: LocationType.InPerson, label: "in_person_meeting" },
{ value: LocationType.Link, label: "link_meeting" },
{ value: LocationType.Phone, label: "attendee_phone_number" },
{ value: LocationType.UserPhone, label: "host_phone_number" },
];
export function getLocationOptions(integrations: AppMeta, t: TFunction) {
const locations = [...defaultLocations];
export function getLocationOptions(integrations: ReturnType<typeof getApps>, t: TFunction) {
const locations: OptionTypeBase[] = [];
defaultLocations.forEach((l) => {
locations.push({
label: l.label,
value: l.type,
});
});
integrations.forEach((app) => {
if (app.locationOption) {
locations.push(app.locationOption);
@ -72,10 +72,10 @@ function getApps(userCredentials: CredentialData[]) {
}
/** Check if app has location option AND add it if user has credentials for it */
if (credentials.length > 0 && appMeta?.locationType) {
if (credentials.length > 0 && appMeta?.appData?.location) {
locationOption = {
value: appMeta.locationType,
label: appMeta.locationLabel || "No label set",
value: appMeta.appData.location.type,
label: appMeta.appData.location.label || "No label set",
disabled: false,
};
}
@ -96,43 +96,10 @@ function getApps(userCredentials: CredentialData[]) {
return apps;
}
export type AppMeta = ReturnType<typeof getApps>;
export function hasIntegrationInstalled(type: App["type"]): boolean {
return ALL_APPS.some((app) => app.type === type && !!app.installed);
}
export function getLocationTypes(): string[] {
return ALL_APPS.reduce((locations, app) => {
if (typeof app.locationType === "string") {
locations.push(app.locationType);
}
return locations;
}, [] as string[]);
}
export function getLocationLabels(t: TFunction) {
const defaultLocationLabels = defaultLocations.reduce((locations, location) => {
if (location.label === "attendee_phone_number") {
locations[location.value] = t("your_number");
return locations;
}
if (location.label === "host_phone_number") {
locations[location.value] = `${t("phone_call")} (${t("number_provided")})`;
return locations;
}
locations[location.value] = t(location.label);
return locations;
}, {} as Record<LocationType, string>);
return ALL_APPS.reduce((locations, app) => {
if (typeof app.locationType === "string") {
locations[app.locationType] = t(app.locationLabel || "No label set");
}
return locations;
}, defaultLocationLabels);
}
export function getAppName(name: string): string | null {
return ALL_APPS_MAP[name as keyof typeof ALL_APPS_MAP]?.name ?? null;
}

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -22,6 +22,6 @@ export const metadata = {
variant: "other",
verified: true,
email: "support@tryvital.io",
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,5 +1,4 @@
import { LocationType } from "@calcom/core/location";
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import config from "./config.json";
@ -11,9 +10,16 @@ export const metadata = {
reviews: 0,
trending: true,
verified: true,
locationType: LocationType.Whereby,
locationLabel: "Whereby Video",
appData: {
location: {
linkType: "static",
type: "integrations:whereby_video",
label: "Whereby Video",
organizerInputPlaceholder: "https://www.whereby.com/cal",
urlRegExp: "^http(s)?:\\/\\/(www\\.)?whereby.com\\/[a-zA-Z0-9]*",
},
},
...config,
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -21,6 +21,6 @@ export const metadata = {
variant: "other",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -1,4 +1,4 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import _package from "./package.json";
@ -20,6 +20,6 @@ export const metadata = {
variant: "other",
verified: true,
email: "help@cal.com",
} as App;
} as AppMeta;
export default metadata;

View File

@ -55,10 +55,10 @@ export default function ZapierSetup(props: IZapierSetupProps) {
<div className="m-auto rounded bg-white p-10">
<div className="flex flex-row">
<div className="mr-5">
<img className="h-11" src="/api/app-store/zapier/icon.svg" alt="Zapier Logo" />
<img className="h-11" src="/api/app-store/typeform/icon.svg" alt="Zapier Logo" />
</div>
<div className="ml-5">
<div className="text-gray-600">{t("setting_up_zapier")}</div>
<div className="text-gray-600">How to integrate a Typeform with Routing Form</div>
{!newApiKey ? (
<>
<div className="mt-1 text-xl">{t("generate_api_key")}:</div>

View File

@ -1,9 +1,9 @@
import type { App } from "@calcom/types/App";
import type { AppMeta } from "@calcom/types/App";
import { LocationType } from "../locations";
import _package from "./package.json";
export const metadata = {
linkType: "dynamic",
name: "Zoom Video",
description: _package.description,
type: "zoom_video",
@ -20,8 +20,14 @@ export const metadata = {
title: "Zoom Video",
trending: true,
email: "help@cal.com",
locationType: LocationType.Zoom,
locationLabel: "Zoom Video",
} as App;
appData: {
location: {
default: false,
linkType: "dynamic",
type: "integrations:zoom",
label: "Zoom Video",
},
},
} as AppMeta;
export default metadata;

View File

@ -4,6 +4,7 @@ import merge from "lodash/merge";
import { v5 as uuidv5 } from "uuid";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
import getApps from "@calcom/app-store/utils";
import prisma from "@calcom/prisma";
import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar";
@ -16,55 +17,15 @@ import type {
} from "@calcom/types/EventManager";
import { createEvent, updateEvent } from "./CalendarManager";
import { LocationType } from "./location";
import { createMeeting, updateMeeting } from "./videoClient";
export const isZoom = (location: string): boolean => {
return location === "integrations:zoom";
};
export const isDaily = (location: string): boolean => {
return location === "integrations:daily";
};
export const isHuddle01 = (location: string): boolean => {
return location === "integrations:huddle01";
};
export const isTandem = (location: string): boolean => {
return location === "integrations:tandem";
};
export const isTeams = (location: string): boolean => {
return location === "integrations:office365_video";
};
export const isJitsi = (location: string): boolean => {
return location === "integrations:jitsi";
};
export const isDedicatedIntegration = (location: string): boolean => {
return (
isZoom(location) ||
isDaily(location) ||
isHuddle01(location) ||
isTandem(location) ||
isJitsi(location) ||
isTeams(location)
);
return location !== "integrations:google:meet" && location.includes("integrations:");
};
export const getLocationRequestFromIntegration = (location: string) => {
if (
/** TODO: Handle this dynamically */
location === LocationType.GoogleMeet.valueOf() ||
location === LocationType.Zoom.valueOf() ||
location === LocationType.Daily.valueOf() ||
location === LocationType.Jitsi.valueOf() ||
location === LocationType.Huddle01.valueOf() ||
location === LocationType.Tandem.valueOf() ||
location === LocationType.Teams.valueOf()
) {
const eventLocationType = getEventLocationTypeFromApp(location);
if (eventLocationType) {
const requestId = uuidv5(location, uuidv5.URL);
return {
@ -84,6 +45,8 @@ export const processLocation = (event: CalendarEvent): CalendarEvent => {
// If location is set to an integration location
// Build proper transforms for evt object
// Extend evt object with those transformations
// TODO: Rely on linkType:"dynamic" here. static links don't send their type. They send their URL directly.
if (event.location?.includes("integration")) {
const maybeLocationRequestObject = getLocationRequestFromIntegration(event.location);

View File

@ -1,5 +1,7 @@
import { TFunction } from "next-i18next";
import { guessEventLocationType } from "@calcom/app-store/locations";
type EventNameObjectType = {
attendeeName: string;
eventType: string;
@ -18,35 +20,12 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView
});
let eventName = eventNameObj.eventName;
let locationString = "";
let locationString = eventNameObj.location || "";
if (eventNameObj.eventName.includes("{LOCATION}")) {
switch (eventNameObj.location) {
case "inPerson":
locationString = "In Person";
break;
case "userPhone":
case "phone":
locationString = "Phone";
break;
case "integrations:daily":
locationString = "Cal Video";
break;
case "integrations:zoom":
locationString = "Zoom";
break;
case "integrations:huddle01":
locationString = "Huddle01";
break;
case "integrations:tandem":
locationString = "Tandem";
break;
case "integrations:office365_video":
locationString = "MS Teams";
break;
case "integrations:jitsi":
locationString = "Jitsi";
break;
const eventLocationType = guessEventLocationType(eventNameObj.location);
if (eventLocationType) {
locationString = eventLocationType.label;
}
eventName = eventName.replace("{LOCATION}", locationString);
}

View File

@ -1,7 +1,8 @@
import type { TFunction } from "next-i18next";
import { getAppName } from "@calcom/app-store/utils";
import { getVideoCallPassword, getVideoCallUrl, getProviderName } from "@calcom/lib/CalEventParser";
import { guessEventLocationType } from "@calcom/app-store/locations";
import { getVideoCallPassword, getVideoCallUrl } from "@calcom/lib/CalEventParser";
import logger from "@calcom/lib/logger";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { Info } from "./Info";
@ -9,89 +10,74 @@ import { LinkIcon } from "./LinkIcon";
export function LocationInfo(props: { calEvent: CalendarEvent; t: TFunction }) {
const { t } = props;
const providerName =
(props.calEvent.location && getAppName(props.calEvent.location)) || getProviderName(props.calEvent);
if (props.calEvent.videoCallData) {
const meetingId = props.calEvent.videoCallData.id;
const meetingPassword = getVideoCallPassword(props.calEvent);
const meetingUrl = getVideoCallUrl(props.calEvent);
// We would not be able to determine provider name for DefaultEventLocationTypes
const providerName = guessEventLocationType(props.calEvent.location)?.label;
logger.debug(`LocationInfo: ${JSON.stringify(props.calEvent)} ${providerName}`);
return (
<Info
label={t("where")}
withSpacer
description={
meetingUrl ? (
<a
href={meetingUrl}
target="_blank"
title={t("meeting_url")}
style={{ color: "#3E3E3E" }}
rel="noreferrer">
{providerName} <LinkIcon />
</a>
) : (
<>{t("something_went_wrong")}</>
)
}
extraInfo={
<>
{meetingId && (
<div style={{ color: "#494949", fontWeight: 400, lineHeight: "24px" }}>
<>
{t("meeting_id")}: <span>{meetingId}</span>
</>
</div>
)}
{meetingPassword && (
<div style={{ color: "#494949", fontWeight: 400, lineHeight: "24px" }}>
<>
{t("meeting_password")}: <span>{meetingPassword}</span>
</>
</div>
)}
{meetingUrl && (
<div style={{ color: "#494949", fontWeight: 400, lineHeight: "24px" }}>
<>
{t("meeting_url")}:{" "}
<a href={meetingUrl} title={t("meeting_url")} style={{ color: "#3E3E3E" }}>
{meetingUrl}
</a>
</>
</div>
)}
</>
}
/>
);
const location = props.calEvent.location;
let meetingUrl = location?.search(/^https?:/) !== -1 ? location : undefined;
if (props.calEvent) {
meetingUrl = getVideoCallUrl(props.calEvent) || meetingUrl;
}
if (props.calEvent.additionalInformation?.hangoutLink) {
const hangoutLink: string = props.calEvent.additionalInformation.hangoutLink;
const isPhone = location?.startsWith("+");
// Because of location being a value here, we can determine the app that generated the location only for Dynamic Link based apps where the value is integrations:*
// For static link based location apps, the value is that URL itself. So, it is not straightforward to determine the app that generated the location.
// If we know the App we can always provide the name of the app like we do it for Google Hangout/Google Meet
if (meetingUrl) {
return (
<Info
label={t("where")}
withSpacer
description={
<a
href={hangoutLink}
href={meetingUrl}
target="_blank"
title={t("meeting_url")}
style={{ color: "#3E3E3E" }}
rel="noreferrer">
Google <LinkIcon />
{providerName || "Link"} <LinkIcon />
</a>
}
extraInfo={
meetingUrl && (
<div style={{ color: "#494949", fontWeight: 400, lineHeight: "24px" }}>
<>
{t("meeting_url")}:{" "}
<a href={meetingUrl} title={t("meeting_url")} style={{ color: "#3E3E3E" }}>
{meetingUrl}
</a>
</>
</div>
)
}
/>
);
}
if (isPhone) {
return (
<Info
label={t("where")}
withSpacer
description={
<a href={"tel:" + location} title="Phone" style={{ color: "#3E3E3E" }}>
{location}
</a>
}
/>
);
}
return (
<Info
label={t("where")}
withSpacer
description={providerName || props.calEvent.location}
description={providerName || location}
extraInfo={
(providerName === "Zoom" || providerName === "Google") && props.calEvent.requiresConfirmation ? (
<p style={{ color: "#494949", fontWeight: 400, lineHeight: "24px" }}>

View File

@ -4,7 +4,7 @@ import Head from "next/head";
import { FC, useEffect, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { LocationOptionsToString } from "@calcom/app-store/locations";
import { getSuccessPageLocationMessage } from "@calcom/app-store/locations";
import getStripe from "@calcom/app-store/stripepayment/lib/client";
import dayjs from "@calcom/dayjs";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
@ -103,7 +103,7 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
<>
<div className="font-medium">{t("where")}</div>
<div className="col-span-2 mb-6">
{LocationOptionsToString(props.booking.location, t)}
{getSuccessPageLocationMessage(props.booking.location, t)}
</div>
</>
)}

View File

@ -1,3 +1,4 @@
import { guessEventLocationType } from "@calcom/app-store/locations";
import { Dayjs } from "@calcom/dayjs";
import { Prisma } from "@calcom/prisma/client";
@ -18,38 +19,7 @@ const customTemplate = async (text: string, variables: VariablesType, locale: st
let locationString = variables.location || "";
if (text.includes("{LOCATION}")) {
switch (variables.location) {
case "integrations:google:meet":
locationString = "Google Meet";
break;
case "integrations:daily":
locationString = "Cal Video";
break;
case "integrations:zoom":
locationString = "Zoom";
break;
case "integrations:huddle01":
locationString = "Huddle01";
break;
case "integrations:tandem":
locationString = "Tandem";
break;
case "integrations:office365_video":
locationString = "MS Teams";
break;
case "integrations:jitsi":
locationString = "Jitsi";
break;
case "integrations:whereby_video":
locationString = "Whereby";
break;
case "integrations:around_video":
locationString = "Around";
break;
case "integrations:riverside_video":
locationString = "Riverside";
break;
}
locationString = guessEventLocationType(locationString)?.label || "";
}
let dynamicText = text

View File

@ -1,6 +1,7 @@
import type { EventTypeCustomInput } from "@prisma/client";
import { PeriodType, Prisma, SchedulingType, UserPlan } from "@prisma/client";
import { DailyLocationType } from "@calcom/app-store/locations";
import { userSelect } from "@calcom/prisma/selects";
type User = Prisma.UserGetPayload<typeof userSelect>;
@ -59,7 +60,7 @@ const commons = {
periodType: PeriodType.UNLIMITED,
periodDays: null,
slotInterval: null,
locations: [{ type: "integrations:daily" }],
locations: [{ type: DailyLocationType }],
customInputs,
disableGuests: true,
minimumBookingNotice: 120,

View File

@ -15,6 +15,7 @@ function defaultResponder<T>(f: Handle<T>) {
ok = true;
if (result) res.json(result);
} catch (err) {
console.error(err);
const error = getServerErrorFromUnknown(err);
res.statusCode = error.statusCode;
res.json({ message: error.message });

View File

@ -2,38 +2,50 @@
{
"/*": "This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually",
"dirName": "routing_forms",
"categories": ["other"],
"categories": [
"other"
],
"slug": "routing_forms",
"type": "routing_forms_other"
},
{
"dirName": "whereby",
"categories": ["video"],
"categories": [
"video"
],
"slug": "whereby",
"type": "whereby_video"
},
{
"dirName": "around",
"categories": ["video"],
"categories": [
"video"
],
"slug": "around",
"type": "around_video"
},
{
"dirName": "riverside",
"categories": ["video"],
"categories": [
"video"
],
"slug": "riverside",
"type": "riverside_video"
},
{
"dirName": "typeform",
"categories": ["other"],
"categories": [
"other"
],
"slug": "typeform",
"type": "typeform_other"
},
{
"dirName": "ping",
"categories": ["video"],
"categories": [
"video"
],
"slug": "ping",
"type": "ping_video"
}
]
]

View File

@ -1,6 +1,9 @@
import { BookingStatus, MembershipRole, Prisma, UserPermissionRole, UserPlan } from "@prisma/client";
import { uuid } from "short-uuid";
import dailyMeta from "@calcom/app-store/dailyvideo/_metadata";
import googleMeetMeta from "@calcom/app-store/googlevideo/_metadata";
import zoomMeta from "@calcom/app-store/zoomvideo/_metadata";
import dayjs from "@calcom/dayjs";
import { hashPassword } from "@calcom/lib/auth";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
@ -263,19 +266,19 @@ async function main() {
title: "Zoom Event",
slug: "zoom",
length: 60,
locations: [{ type: "integrations:zoom" }],
locations: [{ type: zoomMeta.appData?.location.type }],
},
{
title: "Daily Event",
slug: "daily",
length: 60,
locations: [{ type: "integrations:daily" }],
locations: [{ type: dailyMeta.appData?.location.type }],
},
{
title: "Google Meet",
slug: "google-meet",
length: 60,
locations: [{ type: "integrations:google:meet" }],
locations: [{ type: googleMeetMeta.appData?.location.type }],
},
{
title: "Yoga class",

View File

@ -17,7 +17,9 @@ export enum Frequency {
export const eventTypeLocations = z.array(
z.object({
type: z.nativeEnum(LocationType),
// TODO: Couldn't find a way to make it a union of types from App Store locations
// Creating a dynamic union by iterating over the object doesn't seem to make TS happy
type: z.string(),
address: z.string().optional(),
link: z.string().url().optional(),
displayLocationPublicly: z.boolean().optional(),

View File

@ -8,6 +8,7 @@ import stripe, { closePayments } from "@calcom/app-store/stripepayment/lib/serve
import getApps, { getLocationOptions } from "@calcom/app-store/utils";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { DailyLocationType } from "@calcom/core/location";
import dayjs from "@calcom/dayjs";
import { sendCancelledEmails, sendFeedbackEmail } from "@calcom/emails";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
@ -1031,7 +1032,7 @@ const loggedInViewerRouter = createProtectedRouter()
const updatedLocations = locations.map((location: { type: string }) => {
if (location.type.includes(integrationQuery)) {
return { type: "integrations:daily" };
return { type: DailyLocationType };
}
return location;
});

View File

@ -1,7 +1,7 @@
import { SchedulingType } from "@prisma/client";
import { z } from "zod";
import { LocationType } from "@calcom/app-store/locations";
import { DailyLocationType } from "@calcom/app-store/locations";
import EventManager from "@calcom/core/EventManager";
import dayjs from "@calcom/dayjs";
import { sendLocationChangeEmails } from "@calcom/emails";
@ -74,7 +74,7 @@ export const bookingsRouter = createProtectedRouter()
})
.mutation("editLocation", {
input: commonBookingSchema.extend({
newLocation: z.string().transform((val) => val || LocationType.Daily),
newLocation: z.string().transform((val) => val || DailyLocationType),
}),
async resolve({ ctx, input }) {
const { bookingId, newLocation: location } = input;

View File

@ -3,6 +3,7 @@ import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { z } from "zod";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { DailyLocationType } from "@calcom/app-store/locations";
import { stripeDataSchema } from "@calcom/app-store/stripepayment/lib/server";
import { _DestinationCalendarModel, _EventTypeCustomInputModel, _EventTypeModel } from "@calcom/prisma/zod";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
@ -122,7 +123,7 @@ export const eventTypesRouter = createProtectedRouter()
const appKeys = await getAppKeysFromSlug("daily-video");
if (typeof appKeys.api_key === "string") {
data.locations = [{ type: "integrations:daily" }];
data.locations = [{ type: DailyLocationType }];
}
if (teamId && schedulingType) {

View File

@ -1,6 +1,38 @@
import type { Prisma } from "@prisma/client";
import type { LocationType } from "@calcom/app-store/locations";
import { Optional } from "./utils";
type CommonProperties = {
default?: false;
type: string;
label: string;
messageForOrganizer?: string;
iconUrl?: string;
variable?: "locationLink";
defaultValueVariable?: "link";
attendeeInputType?: null;
attendeeInputPlaceholder?: null;
};
type StaticLinkBasedEventLocation = {
linkType: "static";
urlRegExp: string;
organizerInputPlaceholder?: string;
organizerInputType?: "text" | "phone";
} & CommonProperties;
type DynamicLinkBasedEventLocation = {
linkType: "dynamic";
urlRegExp?: null;
organizerInputType?: null;
organizerInputPlaceholder?: null;
} & CommonProperties;
export type EventLocationTypeFromAppMeta = StaticLinkBasedEventLocation | DynamicLinkBasedEventLocation;
type EventLocationAppData = {
location: EventLocationTypeFromAppMeta;
};
/**
* This is the definition for an app store's app metadata.
@ -37,8 +69,16 @@ export interface App {
variant: "calendar" | "payment" | "conferencing" | "video" | "other" | "other_calendar";
/** The slug for the app store public page inside `/apps/[slug] */
slug: string;
/** The category to which this app belongs, currently we have `calendar`, `payment` or `video` */
/*
* @deprecated Use categories
*/
category: string;
/** The category to which this app belongs, currently we have `calendar`, `payment` or `video` */
categories?: string[];
/** An absolute url to the app logo */
logo: string;
/** Company or individual publishing this app */
@ -62,10 +102,7 @@ export interface App {
isGlobal?: boolean;
/** A contact email, mainly to ask for support */
email: string;
/** Add this value as a posible location option in event types */
locationType?: LocationType;
/** If the app adds a location, how should it be displayed? */
locationLabel?: string;
/** Needed API Keys (usually for global apps) */
key?: Prisma.JsonValue;
/** Needed API Keys (usually for global apps) */
@ -78,4 +115,7 @@ export interface App {
commission?: number;
licenseRequired?: boolean;
isProOnly?: boolean;
appData?: EventLocationAppData;
}
export type AppMeta = Optional<App, "rating" | "trending" | "reviews" | "verified">;

View File

@ -4,7 +4,6 @@ import React from "react";
import classNames from "@calcom/lib/classNames";
import { Badge } from "./Badge";
import Button from "./Button";
export type BaseCardProps = {

View File

@ -7116,12 +7116,12 @@ acorn@^8.0.0, acorn@^8.0.4, acorn@^8.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
acorn@^8.5.0, acorn@^8.7.1:
acorn@^8.5.0:
version "8.7.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
acorn@^8.6.0, acorn@^8.8.0:
acorn@^8.6.0, acorn@^8.7.1, acorn@^8.8.0:
version "8.8.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==