Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2988870d5 | ||
|
|
f0ea8d30ca | ||
|
|
c3909ccc70 | ||
|
|
01e88b3807 | ||
|
|
effb9d56d9 | ||
|
|
3bbbc80511 | ||
|
|
19128fb08e | ||
|
|
0945bbe5cf | ||
|
|
ced6975fc8 | ||
|
|
fb436996c0 | ||
|
|
50f1fe544e | ||
|
|
746643bf8e | ||
|
|
65a69ef1e4 | ||
|
|
6483182ef6 | ||
|
|
784a91709c | ||
|
|
82a52e065f | ||
|
|
a1f6738cf1 | ||
|
|
a231945842 | ||
|
|
a507d5963c | ||
|
|
92806d5257 | ||
|
|
9440df4445 | ||
|
|
4e0efb76cd |
@@ -81,4 +81,9 @@ VITAL_WEBHOOK_SECRET=
|
||||
VITAL_DEVELOPMENT_MODE="sandbox"
|
||||
# "us" | "eu"
|
||||
VITAL_REGION="us"
|
||||
|
||||
# - ZAPIER
|
||||
# Used for the Zapier integration
|
||||
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/zapier/README.md
|
||||
ZAPIER_INVITE_LINK=""
|
||||
# *********************************************************************************************************
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function App({
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<Shell large>
|
||||
<Shell large isPublic>
|
||||
<div className="-mx-4 md:-mx-8">
|
||||
<div className="bg-gray-50 px-4">
|
||||
<Link href="/apps">
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
export default function Loader() {
|
||||
return (
|
||||
<div className="loader border-brand dark:border-darkmodebrand">
|
||||
<span className="loader-inner bg-brand dark:bg-darkmodebrand"></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export { default } from "@calcom/ui/Loader";
|
||||
|
||||
@@ -31,3 +31,16 @@ function SkeletonItem() {
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export const AvailabilitySelectSkeletonLoader = () => {
|
||||
return (
|
||||
<li className="group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3">
|
||||
<div className="flex-grow truncate text-sm">
|
||||
<div className="flex justify-between">
|
||||
<SkeletonText width="32" height="4"></SkeletonText>
|
||||
<SkeletonText width="4" height="4"></SkeletonText>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,8 +2,6 @@ import React from "react";
|
||||
|
||||
import { SkeletonText } from "@calcom/ui";
|
||||
|
||||
import BookingsShell from "@components/BookingsShell";
|
||||
|
||||
function SkeletonLoader() {
|
||||
return (
|
||||
<ul className="mt-6 animate-pulse divide-y divide-neutral-200 border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
||||
@@ -22,10 +20,9 @@ function SkeletonItem() {
|
||||
<div className="flex-grow truncate text-sm">
|
||||
<div className="flex">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<SkeletonText width="32" height="5" />
|
||||
<SkeletonText width="16" height="4" />
|
||||
<SkeletonText width="16" height="5" />
|
||||
<SkeletonText width="32" height="4" />
|
||||
</div>
|
||||
<SkeletonText width="24" height="5" className="ml-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
|
||||
|
||||
@@ -113,8 +113,8 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
||||
|
||||
telemetry.withJitsu((jitsu) =>
|
||||
jitsu.track(
|
||||
telemetryEventTypes.pageView,
|
||||
collectPageParameters("availability", { isTeamBooking: document.URL.includes("team/") })
|
||||
top !== window ? telemetryEventTypes.embedView : telemetryEventTypes.pageView,
|
||||
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
|
||||
)
|
||||
);
|
||||
}, [telemetry]);
|
||||
|
||||
@@ -90,6 +90,15 @@ const BookingPage = ({
|
||||
const { data: session } = useSession();
|
||||
const isBackgroundTransparent = useIsBackgroundTransparent();
|
||||
|
||||
useEffect(() => {
|
||||
telemetry.withJitsu((jitsu) =>
|
||||
jitsu.track(
|
||||
top !== window ? telemetryEventTypes.embedView : telemetryEventTypes.pageView,
|
||||
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (eventType.metadata.smartContractAddress) {
|
||||
const eventOwner = eventType.users[0];
|
||||
@@ -144,7 +153,7 @@ const BookingPage = ({
|
||||
|
||||
const recurringMutation = useMutation(createRecurringBooking, {
|
||||
onSuccess: async (responseData = []) => {
|
||||
const { attendees = [], recurringEventId } = responseData[0] || {};
|
||||
const { attendees = [], id, recurringEventId } = responseData[0] || {};
|
||||
const location = (function humanReadableLocation(location) {
|
||||
if (!location) {
|
||||
return;
|
||||
@@ -168,6 +177,7 @@ const BookingPage = ({
|
||||
email: attendees[0].email,
|
||||
location,
|
||||
eventName: profile.eventName || "",
|
||||
bookingId: id,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -293,7 +303,7 @@ const BookingPage = ({
|
||||
const bookEvent = (booking: BookingFormValues) => {
|
||||
telemetry.withJitsu((jitsu) =>
|
||||
jitsu.track(
|
||||
telemetryEventTypes.bookingConfirmed,
|
||||
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
|
||||
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
|
||||
)
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PaymentType, Prisma } from "@prisma/client";
|
||||
import Stripe from "stripe";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { createPaymentLink } from "@calcom/stripe/client";
|
||||
@@ -16,8 +17,8 @@ export type PaymentInfo = {
|
||||
id?: string | null;
|
||||
};
|
||||
|
||||
const paymentFeePercentage = process.env.PAYMENT_FEE_PERCENTAGE!;
|
||||
const paymentFeeFixed = process.env.PAYMENT_FEE_FIXED!;
|
||||
let paymentFeePercentage: number | undefined;
|
||||
let paymentFeeFixed: number | undefined;
|
||||
|
||||
export async function handlePayment(
|
||||
evt: CalendarEvent,
|
||||
@@ -33,6 +34,10 @@ export async function handlePayment(
|
||||
uid: string;
|
||||
}
|
||||
) {
|
||||
const appKeys = await getAppKeysFromSlug("stripe");
|
||||
if (typeof appKeys.payment_fee_fixed === "number") paymentFeePercentage = appKeys.payment_fee_fixed;
|
||||
if (typeof appKeys.payment_fee_percentage === "number") paymentFeeFixed = appKeys.payment_fee_percentage;
|
||||
|
||||
const paymentFee = Math.round(
|
||||
selectedEventType.price * parseFloat(`${paymentFeePercentage}`) + parseInt(`${paymentFeeFixed}`)
|
||||
);
|
||||
|
||||
@@ -60,7 +60,7 @@ export default class AttendeeScheduledEmail {
|
||||
// Taking care of recurrence rule beforehand
|
||||
let recurrenceRule: string | undefined = undefined;
|
||||
if (this.recurringEvent?.count) {
|
||||
recurrenceRule = new rrule(this.recurringEvent).toString();
|
||||
recurrenceRule = new rrule(this.recurringEvent).toString().replace("RRULE:", "");
|
||||
}
|
||||
const icsEvent = createEvent({
|
||||
start: dayjs(this.calEvent.startTime)
|
||||
|
||||
@@ -57,7 +57,7 @@ export default class OrganizerScheduledEmail {
|
||||
// Taking care of recurrence rule beforehand
|
||||
let recurrenceRule: string | undefined = undefined;
|
||||
if (this.recurringEvent?.count) {
|
||||
recurrenceRule = new rrule(this.recurringEvent).toString();
|
||||
recurrenceRule = new rrule(this.recurringEvent).toString().replace("RRULE:", "");
|
||||
}
|
||||
const icsEvent = createEvent({
|
||||
start: dayjs(this.calEvent.startTime)
|
||||
|
||||
@@ -13,6 +13,8 @@ export const telemetryEventTypes = {
|
||||
googleLogin: "google_login",
|
||||
samlLogin: "saml_login",
|
||||
samlConfig: "saml_config",
|
||||
embedView: "embed_view",
|
||||
embedBookingConfirmed: "embed_booking_confirmed",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -119,7 +119,10 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
|
||||
useEffect(() => {
|
||||
telemetry.withJitsu((jitsu) =>
|
||||
jitsu.track(telemetryEventTypes.pageView, collectPageParameters("/[user]"))
|
||||
jitsu.track(
|
||||
top !== window ? telemetryEventTypes.embedView : telemetryEventTypes.pageView,
|
||||
collectPageParameters("/[user]")
|
||||
)
|
||||
);
|
||||
}, [telemetry]);
|
||||
return (
|
||||
|
||||
@@ -94,6 +94,13 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
|
||||
if (!users.length) return { notFound: true };
|
||||
const [user] = users;
|
||||
const isDynamicGroupBooking = users.length > 1;
|
||||
|
||||
// Dynamic Group link doesn't need a type but it must have a slug
|
||||
if ((!isDynamicGroupBooking && !context.query.type) || (isDynamicGroupBooking && !eventTypeSlug)) {
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
const eventTypeRaw =
|
||||
usernameList.length > 1
|
||||
? getDefaultEvent(eventTypeSlug)
|
||||
@@ -173,8 +180,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
booking = await getBooking(prisma, context.query.rescheduleUid as string);
|
||||
}
|
||||
|
||||
const isDynamicGroupBooking = users.length > 1;
|
||||
|
||||
const dynamicNames = isDynamicGroupBooking
|
||||
? users.map((user) => {
|
||||
return user.name || "";
|
||||
|
||||
@@ -25,6 +25,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
res.status(404);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(500);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -54,7 +54,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||
type={data.type}
|
||||
logo={data.logo}
|
||||
categories={[data.category]}
|
||||
author="Cal.com"
|
||||
author={data.publisher}
|
||||
feeType={data.feeType || "usage-based"}
|
||||
price={data.price || 0}
|
||||
commission={data.commission || 0}
|
||||
45
apps/web/pages/apps/[slug]/setup.tsx
Normal file
45
apps/web/pages/apps/[slug]/setup.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { InferGetStaticPropsType } from "next";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { AppSetupPage } from "@calcom/app-store/_pages/setup";
|
||||
import { AppSetupPageMap, getStaticProps } from "@calcom/app-store/_pages/setup/_getStaticProps";
|
||||
import prisma from "@calcom/prisma";
|
||||
import Loader from "@calcom/ui/Loader";
|
||||
|
||||
export default function SetupInformation(props: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const router = useRouter();
|
||||
const slug = router.query.slug as string;
|
||||
const { status } = useSession();
|
||||
|
||||
if (status === "loading") {
|
||||
return (
|
||||
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-200">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "unauthenticated") {
|
||||
router.replace({
|
||||
pathname: "/auth/login",
|
||||
query: {
|
||||
callbackUrl: `/apps/${slug}/setup`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return <AppSetupPage slug={slug} {...props} />;
|
||||
}
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const appStore = await prisma.app.findMany({ select: { slug: true } });
|
||||
const paths = appStore.filter((a) => a.slug in AppSetupPageMap).map((app) => app.slug);
|
||||
|
||||
return {
|
||||
paths: paths.map((slug) => ({ params: { slug } })),
|
||||
fallback: false,
|
||||
};
|
||||
};
|
||||
|
||||
export { getStaticProps };
|
||||
@@ -1,38 +0,0 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import _zapierMetadata from "@calcom/app-store/zapier/_metadata";
|
||||
import { ZapierSetup } from "@calcom/app-store/zapier/components";
|
||||
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import Loader from "@components/Loader";
|
||||
|
||||
export default function SetupInformation() {
|
||||
const router = useRouter();
|
||||
const appName = router.query.appName;
|
||||
const { status } = useSession();
|
||||
|
||||
if (status === "loading") {
|
||||
return (
|
||||
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-200">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "unauthenticated") {
|
||||
router.replace({
|
||||
pathname: "/auth/login",
|
||||
query: {
|
||||
callbackUrl: `/apps/setup/${appName}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (appName === _zapierMetadata.name.toLowerCase() && status === "authenticated") {
|
||||
return <ZapierSetup trpc={trpc}></ZapierSetup>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -60,10 +60,7 @@ export default function Bookings() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Shell
|
||||
heading={t("bookings")}
|
||||
subtitle={t("bookings_description")}
|
||||
customLoader={<SkeletonLoader></SkeletonLoader>}>
|
||||
<Shell heading={t("bookings")} subtitle={t("bookings_description")} customLoader={<SkeletonLoader />}>
|
||||
<WipeMyCalActionButton trpc={trpc} bookingStatus={status} bookingsEmpty={isEmpty} />
|
||||
<BookingsShell>
|
||||
<div className="-mx-4 flex flex-col sm:mx-auto">
|
||||
|
||||
@@ -60,6 +60,7 @@ import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||
import Loader from "@components/Loader";
|
||||
import Shell from "@components/Shell";
|
||||
import { UpgradeToProDialog } from "@components/UpgradeToProDialog";
|
||||
import { AvailabilitySelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||
import RecurringEventController from "@components/eventtype/RecurringEventController";
|
||||
import CustomInputTypeForm from "@components/pages/eventtypes/CustomInputTypeForm";
|
||||
@@ -164,6 +165,7 @@ const AvailabilitySelect = ({
|
||||
return (
|
||||
<QueryCell
|
||||
query={query}
|
||||
customLoader={<AvailabilitySelectSkeletonLoader />}
|
||||
success={({ data }) => {
|
||||
const options = data.schedules.map((schedule) => ({
|
||||
value: schedule.id,
|
||||
|
||||
@@ -78,7 +78,10 @@ const Item = ({ type, group, readOnly }: any) => {
|
||||
return (
|
||||
<Link href={"/event-types/" + type.id}>
|
||||
<a
|
||||
className="flex-grow truncate text-sm"
|
||||
className={classNames(
|
||||
"flex-grow truncate text-sm ",
|
||||
type.$disabled && "pointer-events-none cursor-not-allowed opacity-30"
|
||||
)}
|
||||
title={`${type.title} ${type.description ? `– ${type.description}` : ""}`}>
|
||||
<div>
|
||||
<span
|
||||
@@ -207,17 +210,19 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
{types.map((type, index) => (
|
||||
<li
|
||||
key={type.id}
|
||||
className={classNames(
|
||||
type.$disabled && "pointer-events-none cursor-not-allowed select-none opacity-30"
|
||||
)}
|
||||
className={classNames(type.$disabled && "select-none")}
|
||||
data-disabled={type.$disabled ? 1 : 0}>
|
||||
<div
|
||||
className={classNames(
|
||||
"flex items-center justify-between hover:bg-neutral-50 ",
|
||||
type.$disabled && "pointer-events-none"
|
||||
type.$disabled && "hover:bg-white"
|
||||
)}>
|
||||
<div className="group flex w-full items-center justify-between px-4 py-4 hover:bg-neutral-50 sm:px-6">
|
||||
{types.length > 1 && (
|
||||
<div
|
||||
className={classNames(
|
||||
"group flex w-full items-center justify-between px-4 py-4 hover:bg-neutral-50 sm:px-6",
|
||||
type.$disabled && "hover:bg-white"
|
||||
)}>
|
||||
{types.length > 1 && !type.$disabled && (
|
||||
<>
|
||||
<button
|
||||
className="invisible absolute left-1/2 -mt-4 mb-4 -ml-4 hidden h-7 w-7 scale-0 rounded-full border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100 sm:left-[19px] sm:ml-0 sm:block"
|
||||
@@ -238,7 +243,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
{type.users?.length > 1 && (
|
||||
<AvatarGroup
|
||||
border="border-2 border-white"
|
||||
className="relative top-1 right-3"
|
||||
className={classNames("relative top-1 right-3", type.$disabled && " opacity-30")}
|
||||
size={8}
|
||||
truncateAfter={4}
|
||||
items={type.users.map((organizer) => ({
|
||||
@@ -247,28 +252,38 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
<Tooltip content={t("preview")}>
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${group.profile.slug}/${type.slug}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="btn-icon appearance-none">
|
||||
<ExternalLinkIcon className="h-5 w-5 group-hover:text-black" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<div
|
||||
className={classNames(
|
||||
"flex justify-between space-x-2 rtl:space-x-reverse ",
|
||||
type.$disabled && "pointer-events-none cursor-not-allowed"
|
||||
)}>
|
||||
<Tooltip content={t("preview")}>
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${group.profile.slug}/${type.slug}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={classNames("btn-icon appearance-none", type.$disabled && " opacity-30")}>
|
||||
<ExternalLinkIcon
|
||||
className={classNames("h-5 w-5", !type.$disabled && "group-hover:text-black")}
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={t("copy_link")}>
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(t("link_copied"), "success");
|
||||
navigator.clipboard.writeText(
|
||||
`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${group.profile.slug}/${type.slug}`
|
||||
);
|
||||
}}
|
||||
className="btn-icon">
|
||||
<LinkIcon className="h-5 w-5 group-hover:text-black" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content={t("copy_link")}>
|
||||
<button
|
||||
onClick={() => {
|
||||
showToast(t("link_copied"), "success");
|
||||
navigator.clipboard.writeText(
|
||||
`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${group.profile.slug}/${type.slug}`
|
||||
);
|
||||
}}
|
||||
className={classNames("btn-icon", type.$disabled && " opacity-30")}>
|
||||
<LinkIcon
|
||||
className={classNames("h-5 w-5", !type.$disabled && "group-hover:text-black")}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger
|
||||
className="h-10 w-10 cursor-pointer rounded-sm border border-transparent text-neutral-500 hover:border-gray-300 hover:text-neutral-900 focus:border-gray-300"
|
||||
@@ -282,7 +297,10 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
type="button"
|
||||
size="sm"
|
||||
color="minimal"
|
||||
className="w-full rounded-none"
|
||||
className={classNames(
|
||||
"w-full rounded-none",
|
||||
type.$disabled && " pointer-events-none cursor-not-allowed opacity-30"
|
||||
)}
|
||||
StartIcon={PencilIcon}>
|
||||
{" "}
|
||||
{t("edit")}
|
||||
@@ -294,7 +312,10 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
type="button"
|
||||
color="minimal"
|
||||
size="sm"
|
||||
className="w-full rounded-none"
|
||||
className={classNames(
|
||||
"w-full rounded-none",
|
||||
type.$disabled && " pointer-events-none cursor-not-allowed opacity-30"
|
||||
)}
|
||||
data-testid={"event-type-duplicate-" + type.id}
|
||||
StartIcon={DuplicateIcon}
|
||||
onClick={() => openModal(group, type)}>
|
||||
@@ -304,7 +325,10 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
<DropdownMenuItem>
|
||||
<EmbedButton
|
||||
dark
|
||||
className="w-full rounded-none"
|
||||
className={classNames(
|
||||
"w-full rounded-none",
|
||||
type.$disabled && " pointer-events-none cursor-not-allowed opacity-30"
|
||||
)}
|
||||
eventTypeId={type.id}></EmbedButton>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
|
||||
@@ -35,6 +35,7 @@ import useTheme from "@lib/hooks/useTheme";
|
||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||
import { isSuccessRedirectAvailable } from "@lib/isSuccessRedirectAvailable";
|
||||
import prisma from "@lib/prisma";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||
import { isBrowserLocale24h } from "@lib/timeFormat";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
@@ -172,6 +173,15 @@ export default function Success(props: SuccessProps) {
|
||||
|
||||
const eventName = getEventName(eventNameObject);
|
||||
const needsConfirmation = eventType.requiresConfirmation && reschedule != "true";
|
||||
const telemetry = useTelemetry();
|
||||
useEffect(() => {
|
||||
telemetry.withJitsu((jitsu) =>
|
||||
jitsu.track(
|
||||
top !== window ? telemetryEventTypes.embedView : telemetryEventTypes.pageView,
|
||||
collectPageParameters("/success")
|
||||
)
|
||||
);
|
||||
}, [telemetry]);
|
||||
|
||||
useEffect(() => {
|
||||
const users = eventType.users;
|
||||
@@ -300,14 +310,6 @@ export default function Success(props: SuccessProps) {
|
||||
<div className="col-span-2 mb-6">{eventName}</div>
|
||||
<div className="font-medium">{t("when")}</div>
|
||||
<div className="col-span-2 mb-6">
|
||||
{date.format("MMMM DD, YYYY")}
|
||||
<br />
|
||||
{date.format("LT")} - {date.add(props.eventType.length, "m").format("LT")}{" "}
|
||||
<span className="text-bookinglight">
|
||||
({localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()})
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<RecurringBookings
|
||||
isReschedule={reschedule === "true"}
|
||||
eventType={props.eventType}
|
||||
@@ -317,7 +319,7 @@ export default function Success(props: SuccessProps) {
|
||||
/>
|
||||
</div>
|
||||
<div className="font-medium">{t("who")}</div>
|
||||
<div className="col-span-2">
|
||||
<div className="col-span-2 mb-6">
|
||||
{bookingInfo?.user && (
|
||||
<div className="mb-3">
|
||||
<p>{bookingInfo.user.name}</p>
|
||||
@@ -546,9 +548,9 @@ function RecurringBookings({
|
||||
{eventType.recurringEvent?.count &&
|
||||
recurringBookings.slice(0, 4).map((dateStr, idx) => (
|
||||
<div key={idx} className="mb-2">
|
||||
{dayjs(dateStr).format("dddd, DD MMMM YYYY")}
|
||||
{dayjs(dateStr).format("MMMM DD, YYYY")}
|
||||
<br />
|
||||
{dayjs(dateStr).format(is24h ? "H:mm" : "h:mma")} - {eventType.length} mins{" "}
|
||||
{dayjs(dateStr).format("LT")} - {dayjs(dateStr).add(eventType.length, "m").format("LT")}{" "}
|
||||
<span className="text-bookinglight">
|
||||
({localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()})
|
||||
</span>
|
||||
@@ -565,9 +567,9 @@ function RecurringBookings({
|
||||
{eventType.recurringEvent?.count &&
|
||||
recurringBookings.slice(4).map((dateStr, idx) => (
|
||||
<div key={idx} className="mb-2">
|
||||
{dayjs(dateStr).format("dddd, DD MMMM YYYY")}
|
||||
{dayjs(dateStr).format("MMMM DD, YYYY")}
|
||||
<br />
|
||||
{dayjs(dateStr).format(is24h ? "H:mm" : "h:mma")} - {eventType.length} mins{" "}
|
||||
{dayjs(dateStr).format("LT")} - {dayjs(dateStr).add(eventType.length, "m").format("LT")}{" "}
|
||||
<span className="text-bookinglight">
|
||||
({localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()})
|
||||
</span>
|
||||
@@ -579,9 +581,9 @@ function RecurringBookings({
|
||||
</>
|
||||
) : !eventType.recurringEvent.freq ? (
|
||||
<>
|
||||
{date.format("dddd, DD MMMM YYYY")}
|
||||
{date.format("MMMM DD, YYYY")}
|
||||
<br />
|
||||
{date.format(is24h ? "H:mm" : "h:mma")} - {eventType.length} mins{" "}
|
||||
{date.format("LT")} - {date.add(eventType.length, "m").format("LT")}{" "}
|
||||
<span className="text-bookinglight">
|
||||
({localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()})
|
||||
</span>
|
||||
|
||||
@@ -1,44 +1,27 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("App Store - Authed", () => {
|
||||
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||
test("Browse apple-calendar and try to install", async ({ page }) => {
|
||||
await page.goto("/apps");
|
||||
page.click('[data-testid="app-store-category-calendar"]');
|
||||
await page.waitForNavigation({
|
||||
url: (url) => {
|
||||
console.log(url, url.pathname);
|
||||
return url.pathname.includes("apps/categories/calendar");
|
||||
},
|
||||
});
|
||||
page.click('[data-testid="app-store-app-card-apple-calendar"]');
|
||||
await page.waitForNavigation({
|
||||
url: (url) => {
|
||||
return url.pathname.includes("apps/apple-calendar");
|
||||
},
|
||||
});
|
||||
await page.click('[data-testid="app-store-category-calendar"]');
|
||||
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
|
||||
await page.click('[data-testid="install-app-button"]');
|
||||
await expect(page.locator(`text=Connect to Apple Server`)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("App Store - Unauthed", () => {
|
||||
test("Browse apple-calendar and try to install", async ({ page }) => {
|
||||
await page.goto("/apps");
|
||||
|
||||
await page.waitForSelector("[data-testid=dashboard-shell]");
|
||||
await page.click('[data-testid="app-store-category-calendar"]');
|
||||
if (!page.url().includes("apps/categories/calendar")) {
|
||||
await page.waitForNavigation({
|
||||
url: (url) => {
|
||||
console.log(url, url.pathname);
|
||||
return url.pathname.includes("apps/categories/calendar");
|
||||
},
|
||||
});
|
||||
}
|
||||
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
|
||||
await page.waitForNavigation({
|
||||
url: (url) => {
|
||||
return url.pathname.includes("/auth/login");
|
||||
},
|
||||
});
|
||||
await page.click('[data-testid="install-app-button"]');
|
||||
await expect(page.locator(`[data-testid="login-form"]`)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
todo,
|
||||
} from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("free user", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/free");
|
||||
@@ -44,11 +46,7 @@ test.describe("free user", () => {
|
||||
await bookTimeSlot(page);
|
||||
|
||||
// Make sure we're navigated to the success page
|
||||
await page.waitForNavigation({
|
||||
url(url) {
|
||||
return url.pathname.endsWith("/success");
|
||||
},
|
||||
});
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
// return to same time spot booking page
|
||||
await page.goto(bookingUrl);
|
||||
@@ -98,11 +96,7 @@ test.describe("pro user", () => {
|
||||
await bookTimeSlot(page);
|
||||
|
||||
// Make sure we're navigated to the success page
|
||||
await page.waitForNavigation({
|
||||
url(url) {
|
||||
return url.pathname.endsWith("/success");
|
||||
},
|
||||
});
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
});
|
||||
test("can reschedule a booking", async ({ page }) => {
|
||||
await bookFirstEvent(page);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page, test } from "@playwright/test";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { deleteAllBookingsByEmail } from "./lib/teardown";
|
||||
import {
|
||||
@@ -28,13 +28,7 @@ test.describe("dynamic booking", () => {
|
||||
await page.click('[data-testid="event-type-link"]');
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
|
||||
// Make sure we're navigated to the success page
|
||||
await page.waitForNavigation({
|
||||
url(url) {
|
||||
return url.pathname.endsWith("/success");
|
||||
},
|
||||
});
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
});
|
||||
|
||||
test("can reschedule a booking", async ({ page }) => {
|
||||
@@ -58,6 +52,7 @@ test.describe("dynamic booking", () => {
|
||||
return url.pathname === "/success" && url.searchParams.get("reschedule") === "true";
|
||||
},
|
||||
});
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Can cancel the recently created booking", async ({ page }) => {
|
||||
|
||||
@@ -79,6 +79,8 @@ async function expectToContainValidPreviewIframe(
|
||||
);
|
||||
}
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Embed Code Generator Tests", () => {
|
||||
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { expect, Locator, test } from "@playwright/test";
|
||||
import { randomString } from "../lib/random";
|
||||
import { deleteEventTypeByTitle } from "./lib/teardown";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Event Types tests", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/event-types");
|
||||
|
||||
@@ -46,11 +46,7 @@ test.describe("hash my url", () => {
|
||||
await bookTimeSlot(page);
|
||||
|
||||
// Make sure we're navigated to the success page
|
||||
await page.waitForNavigation({
|
||||
url(url) {
|
||||
return url.pathname.endsWith("/success");
|
||||
},
|
||||
});
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
});
|
||||
|
||||
test("hash regenerates after successful booking", async ({ page }) => {
|
||||
|
||||
@@ -71,11 +71,7 @@ test.describe.serial("Stripe integration", () => {
|
||||
await page.click('button:has-text("Pay now")');
|
||||
|
||||
// Make sure we're navigated to the success page
|
||||
await page.waitForNavigation({
|
||||
url(url) {
|
||||
return url.pathname.endsWith("/success");
|
||||
},
|
||||
});
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
});
|
||||
|
||||
todo("Pending payment booking should not be confirmed by default");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page, test } from "@playwright/test";
|
||||
import { expect, Page, test } from "@playwright/test";
|
||||
import { createServer, IncomingMessage, ServerResponse } from "http";
|
||||
|
||||
export function todo(title: string) {
|
||||
@@ -99,11 +99,7 @@ export async function bookFirstEvent(page: Page) {
|
||||
await page.press('[name="email"]', "Enter");
|
||||
|
||||
// Make sure we're navigated to the success page
|
||||
await page.waitForNavigation({
|
||||
url(url) {
|
||||
return url.pathname.endsWith("/success");
|
||||
},
|
||||
});
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
}
|
||||
|
||||
export const bookTimeSlot = async (page: Page) => {
|
||||
|
||||
@@ -153,11 +153,7 @@ test.describe("Reschedule Tests", async () => {
|
||||
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
|
||||
await page.waitForNavigation({
|
||||
url(url) {
|
||||
return url.pathname.endsWith("/success");
|
||||
},
|
||||
});
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
await expect(page).toHaveURL(/.*success/);
|
||||
|
||||
|
||||
@@ -817,7 +817,7 @@
|
||||
"generate_api_key": "Generate Api Key",
|
||||
"your_unique_api_key": "Your unique API key",
|
||||
"copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.",
|
||||
"zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
|
||||
"zapier_setup_instructions": "<0>Go to: <1>Zapier Invite Link</1></0><1>Log into your Zapier account and create a new Zap.</1><2>Select Cal.com as your Trigger app. Also choose a Trigger event.</2><3>Choose your account and then enter your Unique API Key.</3><4>Test your Trigger.</4><5>You're set!</5>",
|
||||
"install_zapier_app": "Please first install the Zapier App in the app store.",
|
||||
"go_to_app_store": "Go to App Store",
|
||||
"calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions",
|
||||
|
||||
@@ -371,7 +371,6 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||
};
|
||||
const passedBookingsFilter = bookingListingFilters[bookingListingByStatus];
|
||||
const orderBy = bookingListingOrderby[bookingListingByStatus];
|
||||
|
||||
const bookingsQuery = await prisma.booking.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
@@ -440,7 +439,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||
endTime: booking.endTime.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
const bookingsFetched = bookings.length;
|
||||
const seenBookings: Record<string, boolean> = {};
|
||||
|
||||
// Remove duplicate recurring bookings for upcoming status.
|
||||
@@ -461,7 +460,8 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||
}
|
||||
|
||||
let nextCursor: typeof skip | null = skip;
|
||||
if (bookings.length > take) {
|
||||
|
||||
if (bookingsFetched > take) {
|
||||
bookings.shift();
|
||||
nextCursor += bookings.length;
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,6 @@ module.exports = {
|
||||
content: [
|
||||
...base.content,
|
||||
"../../packages/ui/**/*.{js,ts,jsx,tsx}",
|
||||
"../../packages/app-store/**/components/*.{js,ts,jsx,tsx}",
|
||||
"../../packages/app-store/**/{components,pages}/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"heroku-postbuild": "turbo run @calcom/web#build",
|
||||
"lint": "turbo run lint",
|
||||
"lint:report": "turbo run lint:report",
|
||||
"postinstall": "turbo run postinstall",
|
||||
"postinstall": "turbo run post-install",
|
||||
"pre-commit": "lint-staged",
|
||||
"env-check:common": "dotenv-checker --schema .env.example --env .env",
|
||||
"env-check:app-store": "dotenv-checker --schema .env.appStore.example --env .env.appStore",
|
||||
|
||||
9
packages/app-store/_components/DynamicComponent.tsx
Normal file
9
packages/app-store/_components/DynamicComponent.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
export function DynamicComponent<T extends Record<string, any>>(props: { componentMap: T; slug: string }) {
|
||||
const { componentMap, slug, ...rest } = props;
|
||||
|
||||
if (!componentMap[slug]) return null;
|
||||
|
||||
const Component = componentMap[slug];
|
||||
|
||||
return <Component {...rest} />;
|
||||
}
|
||||
20
packages/app-store/_pages/setup/_getStaticProps.tsx
Normal file
20
packages/app-store/_pages/setup/_getStaticProps.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { GetStaticPropsContext } from "next";
|
||||
|
||||
export const AppSetupPageMap = {
|
||||
zapier: import("../../zapier/pages/setup/_getStaticProps"),
|
||||
};
|
||||
|
||||
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||
const { slug } = ctx.params || {};
|
||||
if (typeof slug !== "string") return { notFound: true } as const;
|
||||
|
||||
if (!(slug in AppSetupPageMap)) return { props: {} };
|
||||
|
||||
const page = await AppSetupPageMap[slug as keyof typeof AppSetupPageMap];
|
||||
|
||||
if (!page.getStaticProps) return { props: {} };
|
||||
|
||||
const props = await page.getStaticProps(ctx);
|
||||
|
||||
return props;
|
||||
};
|
||||
13
packages/app-store/_pages/setup/index.tsx
Normal file
13
packages/app-store/_pages/setup/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { DynamicComponent } from "../../_components/DynamicComponent";
|
||||
|
||||
export const AppSetupMap = {
|
||||
zapier: dynamic(() => import("../../zapier/pages/setup")),
|
||||
};
|
||||
|
||||
export const AppSetupPage = (props: { slug: string }) => {
|
||||
return <DynamicComponent<typeof AppSetupMap> componentMap={AppSetupMap} {...props} />;
|
||||
};
|
||||
|
||||
export default AppSetupPage;
|
||||
9
packages/app-store/giphy/README.mdx
Normal file
9
packages/app-store/giphy/README.mdx
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
items:
|
||||
- /api/app-store/giphy/GIPHY1.png
|
||||
- /api/app-store/giphy/GIPHY2.png
|
||||
---
|
||||
|
||||
<Slider items={items} />
|
||||
|
||||
An online database and search engine that allows users to search for and share short looping videos with no sound that resemble animated GIF files. GIPHY is your top source for the best & newest GIFs & Animated Stickers online. Find everything from funny GIFs, reaction GIFs, unique GIFs and more to add to your custom booking page. Located under advanced settings in each event type.
|
||||
BIN
packages/app-store/giphy/static/GIPHY1.png
Normal file
BIN
packages/app-store/giphy/static/GIPHY1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
BIN
packages/app-store/giphy/static/GIPHY2.png
Normal file
BIN
packages/app-store/giphy/static/GIPHY2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
2
packages/app-store/types.d.ts
vendored
2
packages/app-store/types.d.ts
vendored
@@ -1,3 +1,5 @@
|
||||
import { ButtonBaseProps } from "@calcom/ui/Button";
|
||||
|
||||
export type IntegrationOAuthCallbackState = {
|
||||
returnTo: string;
|
||||
};
|
||||
|
||||
@@ -56,9 +56,9 @@ Booking created, Booking rescheduled, Booking cancelled
|
||||
|
||||
Create the other two triggers (booking rescheduled, booking cancelled) exactly like this one, just use the appropriate naming (e.g. booking_rescheduled instead of booking_created)
|
||||
|
||||
### Testing integration
|
||||
### Set ZAPIER_INVITE_LINK
|
||||
|
||||
Use the sharing link under Manage → Sharing to create your first Cal.com trigger in Zapier
|
||||
The invite link can be found under under Manage → Sharing.
|
||||
|
||||
## Localhost
|
||||
|
||||
|
||||
@@ -2,5 +2,4 @@ Workflow automation for everyone. Use the Cal.com Zapier app to trigger your wor
|
||||
|
||||
<br />
|
||||
**After Installation:** You lost your generated API key? Here you can generate a new key and find all information
|
||||
on how to use the installed app: <a href="/apps/setup/zapier">Zapier App Setup</a>
|
||||
|
||||
on how to use the installed app: <a href="/apps/zapier/setup">Zapier App Setup</a>
|
||||
|
||||
@@ -35,5 +35,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(500);
|
||||
}
|
||||
|
||||
return res.status(200).json({ url: "/apps/setup/zapier" });
|
||||
return res.status(200).json({ url: "/apps/zapier/setup" });
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
export { default as ZapierSetup } from "./zapierSetup";
|
||||
export { default as Icon } from "./icon";
|
||||
|
||||
20
packages/app-store/zapier/pages/setup/_getStaticProps.tsx
Normal file
20
packages/app-store/zapier/pages/setup/_getStaticProps.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { GetStaticPropsContext } from "next";
|
||||
|
||||
import getAppKeysFromSlug from "../../../_utils/getAppKeysFromSlug";
|
||||
|
||||
export interface IZapierSetupProps {
|
||||
inviteLink: string;
|
||||
}
|
||||
|
||||
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||
if (typeof ctx.params?.slug !== "string") return { notFound: true } as const;
|
||||
let inviteLink = "";
|
||||
const appKeys = await getAppKeysFromSlug("zapier");
|
||||
if (typeof appKeys.invite_link === "string") inviteLink = appKeys.invite_link;
|
||||
|
||||
return {
|
||||
props: {
|
||||
inviteLink,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -2,28 +2,31 @@ import { ClipboardCopyIcon } from "@heroicons/react/solid";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||
import Loader from "@calcom/web/components/Loader";
|
||||
import { Button, Loader, Tooltip } from "@calcom/ui";
|
||||
|
||||
import Icon from "./icon";
|
||||
/** TODO: Maybe extract this into a package to prevent circular dependencies */
|
||||
import { trpc } from "@calcom/web/lib/trpc";
|
||||
|
||||
interface IZapierSetupProps {
|
||||
trpc: any;
|
||||
import Icon from "../../components/icon";
|
||||
|
||||
export interface IZapierSetupProps {
|
||||
inviteLink: string;
|
||||
}
|
||||
|
||||
const ZAPIER = "zapier";
|
||||
|
||||
export default function ZapierSetup(props: IZapierSetupProps) {
|
||||
const { trpc } = props;
|
||||
const [newApiKey, setNewApiKey] = useState("");
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const integrations = trpc.useQuery(["viewer.integrations"]);
|
||||
// @ts-ignore
|
||||
const oldApiKey = trpc.useQuery(["viewer.apiKeys.findKeyOfType", { appId: ZAPIER }]);
|
||||
|
||||
const deleteApiKey = trpc.useMutation("viewer.apiKeys.delete");
|
||||
const zapierCredentials: { credentialIds: number[] } | undefined = integrations.data?.other?.items.find(
|
||||
(item: { type: string }) => item.type === "zapier_other"
|
||||
@@ -33,6 +36,7 @@ export default function ZapierSetup(props: IZapierSetupProps) {
|
||||
|
||||
async function createApiKey() {
|
||||
const event = { note: "Zapier", expiresAt: null, appId: ZAPIER };
|
||||
// @ts-ignore
|
||||
const apiKey = await utils.client.mutation("viewer.apiKeys.create", event);
|
||||
if (oldApiKey.data) {
|
||||
deleteApiKey.mutate({
|
||||
@@ -91,8 +95,14 @@ export default function ZapierSetup(props: IZapierSetupProps) {
|
||||
</>
|
||||
)}
|
||||
|
||||
<ol className="mt-5 mb-5 mr-5 list-decimal">
|
||||
<ol className="mt-5 mb-5 ml-5 mr-5 list-decimal">
|
||||
<Trans i18nKey="zapier_setup_instructions">
|
||||
<li>
|
||||
Go to:
|
||||
<a href={props.inviteLink} className="text-orange-600 underline">
|
||||
Zapier Invite Link
|
||||
</a>
|
||||
</li>
|
||||
<li>Log into your Zapier account and create a new Zap.</li>
|
||||
<li>Select Cal.com as your Trigger app. Also choose a Trigger event.</li>
|
||||
<li>Choose your account and then enter your Unique API Key.</li>
|
||||
@@ -116,6 +126,7 @@ export default function ZapierSetup(props: IZapierSetupProps) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -83,11 +83,7 @@ export async function bookFirstEvent(username: string, frame: Frame, page: Page)
|
||||
const responseObj = await response.json();
|
||||
const bookingId = responseObj.uid;
|
||||
// Make sure we're navigated to the success page
|
||||
await frame.waitForNavigation({
|
||||
url(url) {
|
||||
return url.pathname.endsWith("/success");
|
||||
},
|
||||
});
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
expect(await page.screenshot()).toMatchSnapshot("success-page.png");
|
||||
return bookingId;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "yarn generate-schemas",
|
||||
"build": "yarn prisma migrate deploy && yarn seed-app-store",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules",
|
||||
"db-deploy": "yarn prisma migrate deploy",
|
||||
"db-migrate": "yarn prisma migrate dev",
|
||||
@@ -13,10 +13,9 @@
|
||||
"db-setup": "run-s db-up db-deploy db-seed",
|
||||
"db-studio": "yarn prisma studio",
|
||||
"db-up": "docker-compose up -d",
|
||||
"deploy": "yarn prisma migrate deploy && yarn seed-app-store",
|
||||
"dx": "yarn db-setup",
|
||||
"generate-schemas": "prisma generate && prisma format",
|
||||
"postinstall": "yarn generate-schemas",
|
||||
"post-install": "yarn generate-schemas",
|
||||
"seed-app-store": "ts-node --transpile-only ./seed-app-store.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -95,7 +95,12 @@ async function main() {
|
||||
webhook_secret: process.env.VITAL_WEBHOOK_SECRET,
|
||||
});
|
||||
}
|
||||
await createApp("zapier", "zapier", ["other"], "zapier_other");
|
||||
|
||||
if (process.env.ZAPIER_INVITE_LINK) {
|
||||
await createApp("zapier", "zapier", ["other"], "zapier_other", {
|
||||
invite_link: process.env.ZAPIER_INVITE_LINK,
|
||||
});
|
||||
}
|
||||
// Web3 apps
|
||||
await createApp("huddle01", "huddle01video", ["web3", "video"], "huddle01_video");
|
||||
await createApp("metamask", "metamask", ["web3"], "metamask_web3");
|
||||
|
||||
7
packages/ui/Loader.tsx
Normal file
7
packages/ui/Loader.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function Loader() {
|
||||
return (
|
||||
<div className="loader border-brand dark:border-darkmodebrand">
|
||||
<span className="loader-inner bg-brand dark:bg-darkmodebrand"></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export { default as Button } from "./Button";
|
||||
export { default as EmptyScreen } from "./EmptyScreen";
|
||||
export { default as Select } from "./form/Select";
|
||||
export { default as Loader } from "./Loader";
|
||||
export * from "./skeleton";
|
||||
export { default as Switch } from "./Switch";
|
||||
export { default as Tooltip } from "./Tooltip";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PlaywrightTestConfig, devices } from "@playwright/test";
|
||||
import { devices, PlaywrightTestConfig } from "@playwright/test";
|
||||
import { addAliases } from "module-alias";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
|
||||
// Add aliases for the paths specified in the tsconfig.json file.
|
||||
@@ -17,11 +18,16 @@ addAliases({
|
||||
const outputDir = path.join(__dirname, "..", "..", "test-results");
|
||||
const testDir = path.join(__dirname, "..", "..", "apps/web/playwright");
|
||||
|
||||
const DEFAULT_NAVIGATION_TIMEOUT = 5000;
|
||||
|
||||
const headless = !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS;
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: 1,
|
||||
workers: 1,
|
||||
workers: os.cpus().length,
|
||||
timeout: 60_000,
|
||||
maxFailures: headless ? 3 : undefined,
|
||||
reporter: [
|
||||
[process.env.CI ? "github" : "list"],
|
||||
["html", { outputFolder: "./playwright/reports/playwright-html-report", open: "never" }],
|
||||
@@ -36,16 +42,20 @@ const config: PlaywrightTestConfig = {
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
use: {
|
||||
baseURL: "http://localhost:3000",
|
||||
baseURL: "http://localhost:3000/",
|
||||
locale: "en-US",
|
||||
trace: "retain-on-failure",
|
||||
headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,
|
||||
headless,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
testDir,
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
/** If navigation takes more than this, then something's wrong, let's fail fast. */
|
||||
navigationTimeout: DEFAULT_NAVIGATION_TIMEOUT,
|
||||
},
|
||||
},
|
||||
/* {
|
||||
name: "firefox",
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
"baseBranch": "origin/main",
|
||||
"globalDependencies": [".env", "packages/prisma/.env"],
|
||||
"pipeline": {
|
||||
"@calcom/prisma#build": {
|
||||
"dependsOn": ["$DATABASE_URL"],
|
||||
"outputs": ["zod/**"]
|
||||
},
|
||||
"@calcom/prisma#db-deploy": {
|
||||
"cache": false,
|
||||
"dependsOn": ["$DATABASE_URL"]
|
||||
@@ -123,7 +119,7 @@
|
||||
"cache": false,
|
||||
"outputs": ["lint-results"]
|
||||
},
|
||||
"postinstall": {},
|
||||
"post-install": {},
|
||||
"start": {},
|
||||
"embed-tests": {
|
||||
"cache": false
|
||||
@@ -136,7 +132,7 @@
|
||||
},
|
||||
"test-e2e": {
|
||||
"cache": false,
|
||||
"dependsOn": ["@calcom/web#test", "@calcom/web#build", "@calcom/prisma#db-reset"]
|
||||
"dependsOn": ["@calcom/prisma#db-reset", "@calcom/web#test", "@calcom/web#build"]
|
||||
},
|
||||
"type-check": {
|
||||
"outputs": []
|
||||
|
||||
Reference in New Issue
Block a user