Merge branch 'feat/organizations' into feat/organizations-banner

This commit is contained in:
Leo Giovanetti 2023-06-14 12:18:34 -03:00
commit 952dde9f54
41 changed files with 271 additions and 53 deletions

View File

@ -8,8 +8,11 @@ import { createRef, forwardRef, useRef, useState } from "react";
import type { ControlProps } from "react-select";
import { components } from "react-select";
import type { BookerLayout } from "@calcom/features/bookings/Booker/types";
import { useFlagMap } from "@calcom/features/flags/context/provider";
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import {
Button,
Dialog,
@ -41,12 +44,13 @@ type PreviewState = {
height: string;
};
theme: Theme;
floatingPopup: Record<string, string>;
floatingPopup: Record<string, string | boolean | undefined>;
elementClick: Record<string, string>;
palette: {
brandColor: string;
};
hideEventTypeDetails: boolean;
layout: BookerLayouts;
};
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
@ -129,11 +133,13 @@ const getEmbedUIInstructionString = ({
theme,
brandColor,
hideEventTypeDetails,
layout,
}: {
apiName: string;
theme?: string;
brandColor: string;
hideEventTypeDetails: boolean;
layout?: string;
}) => {
theme = theme !== "auto" ? theme : undefined;
return getInstructionString({
@ -147,6 +153,7 @@ const getEmbedUIInstructionString = ({
},
},
hideEventTypeDetails: hideEventTypeDetails,
layout,
},
});
};
@ -260,6 +267,7 @@ const getEmbedTypeSpecificString = ({
theme: PreviewState["theme"];
brandColor: string;
hideEventTypeDetails: boolean;
layout?: BookerLayout;
};
if (embedFramework === "react") {
uiInstructionStringArg = {
@ -267,6 +275,7 @@ const getEmbedTypeSpecificString = ({
theme: previewState.theme,
brandColor: previewState.palette.brandColor,
hideEventTypeDetails: previewState.hideEventTypeDetails,
layout: previewState.layout,
};
} else {
uiInstructionStringArg = {
@ -274,6 +283,7 @@ const getEmbedTypeSpecificString = ({
theme: previewState.theme,
brandColor: previewState.palette.brandColor,
hideEventTypeDetails: previewState.hideEventTypeDetails,
layout: previewState.layout,
};
}
if (!frameworkCodes[embedType]) {
@ -626,6 +636,7 @@ const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme;
const ChooseEmbedTypesDialogContent = () => {
const { t } = useLocale();
const router = useRouter();
return (
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
<div className="mb-2">
@ -671,6 +682,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
const router = useRouter();
const iframeRef = useRef<HTMLIFrameElement>(null);
const dialogContentRef = useRef<HTMLDivElement>(null);
const flags = useFlagMap();
const isBookerLayoutsEnabled = flags["booker-layouts"] === true;
const s = (href: string) => {
const searchParams = new URLSearchParams(router.asPath.split("?")[1] || "");
@ -691,7 +704,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
const [previewState, setPreviewState] = useState({
const [previewState, setPreviewState] = useState<PreviewState>({
inline: {
width: "100%",
height: "100%",
@ -703,6 +716,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
palette: {
brandColor: "#000000",
},
layout: BookerLayouts.MONTH_VIEW,
});
const close = () => {
@ -765,6 +779,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
arg: {
theme: previewState.theme,
hideEventTypeDetails: previewState.hideEventTypeDetails,
layout: previewState.layout,
styles: {
branding: {
...previewState.palette,
@ -798,6 +813,12 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
{ value: Theme.light, label: "Light Theme" },
];
const layoutOptions = [
{ value: BookerLayouts.MONTH_VIEW, label: t("bookerlayout_month_view") },
{ value: BookerLayouts.WEEK_VIEW, label: t("bookerlayout_week_view") },
{ value: BookerLayouts.COLUMN_VIEW, label: t("bookerlayout_column_view") },
];
const FloatingPopupPositionOptions = [
{
value: "bottom-right",
@ -1070,6 +1091,27 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</div>
</Label>
))}
{isBookerLayoutsEnabled && (
<Label className="mb-6">
<div className="mb-2">{t("layout")}</div>
<Select
className="w-full"
defaultValue={layoutOptions[0]}
onChange={(option) => {
if (!option) {
return;
}
setPreviewState((previewState) => {
return {
...previewState,
layout: option.value,
};
});
}}
options={layoutOptions}
/>
</Label>
)}
</div>
</CollapsibleContent>
</Collapsible>

View File

@ -26,9 +26,7 @@ export default function useRouterQuery<T extends string>(name: T) {
}
const setQuery = (newValue: string | number | null | undefined) => {
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, {
shallow: true,
});
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
};

View File

@ -8,6 +8,7 @@ export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => {
const ssrResponse = await getServerSideProps(context);
const embed = context.query.embed;
const layout = context.query.layout;
if ("redirect" in ssrResponse) {
// Use a dummy URL https://base as the fallback base URL so that URL parsing works for relative URLs as well.
@ -18,6 +19,8 @@ export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
destinationUrlObj.pathname +
"/embed?" +
destinationUrlObj.searchParams.toString() +
"&layout=" +
layout +
"&embed=" +
embed;

View File

@ -21,6 +21,12 @@ const middleware: NextMiddleware = async (req) => {
return NextResponse.rewrite(url);
}
if (isIpInBanlist(req) && url.pathname !== "/api/nope") {
// DDOS Prevention: Immediately end request with no response - Avoids a redirect as well initiated by NextAuth on invalid callback
req.nextUrl.pathname = "/api/nope";
return NextResponse.redirect(req.nextUrl);
}
if (!url.pathname.startsWith("/api")) {
//
// NOTE: When tRPC hits an error a 500 is returned, when this is received

View File

@ -228,12 +228,19 @@ const nextConfig = {
destination: "/new-booker/:user/:type",
has: [{ type: "cookie", key: "new-booker-enabled" }],
},
{
source: `/:user((?!${pages.join("|")}).*)/:type/embed`,
destination: "/new-booker/:user/:type/embed",
has: [{ type: "cookie", key: "new-booker-enabled" }],
},
{
source: "/team/:slug/:type",
destination: "/new-booker/team/:slug/:type",
has: [{ type: "cookie", key: "new-booker-enabled" }],
},
{
source: "/team/:slug/:type/embed",
destination: "/new-booker/team/:slug/:type/embed",
source: "/d/:link/:slug",
destination: "/new-booker/d/:link/:slug",
has: [{ type: "cookie", key: "new-booker-enabled" }],

View File

@ -6,6 +6,7 @@ import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { classNames } from "@calcom/lib";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import prisma from "@calcom/prisma";
@ -16,8 +17,9 @@ import PageWrapper from "@components/PageWrapper";
type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
return (
<main className="flex h-full min-h-[100dvh] items-center justify-center">
<main className={classNames("flex h-full items-center justify-center", !isEmbed && "min-h-[100dvh]")}>
<BookerSeo
username={user}
eventSlug={slug}
@ -85,6 +87,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
away: false,
trpcState: ssr.dehydrate(),
isBrandingHidden: false,
themeBasis: null,
},
};
}
@ -140,6 +143,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
slug,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
themeBasis: username,
},
};
}

View File

@ -0,0 +1,11 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[type]";
export { default } from "../[type]";
// Somehow these types don't accept the {notFound: true} return type.
// Probably still need to fix this. I don't know why this isn't allowed yet.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const getServerSideProps = withEmbedSsr(_getServerSideProps);

View File

@ -0,0 +1,7 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../../[user]";
export { default } from "../../[user]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);

View File

@ -5,6 +5,7 @@ import { Booker } from "@calcom/atoms";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { classNames } from "@calcom/lib";
import prisma from "@calcom/prisma";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@ -14,8 +15,9 @@ import PageWrapper from "@components/PageWrapper";
type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
return (
<main className="flex h-full min-h-[100dvh] items-center justify-center">
<main className={classNames("flex h-full items-center justify-center", !isEmbed && "min-h-[100dvh]")}>
<BookerSeo
username={user}
eventSlug={slug}
@ -85,6 +87,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
slug: meetingSlug,
trpcState: ssr.dehydrate(),
isBrandingHidden: team?.hideBranding,
themeBasis: null,
},
};
};

View File

@ -0,0 +1,9 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[type]";
export { default } from "../[type]";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const getServerSideProps = withEmbedSsr(_getServerSideProps);

View File

@ -0,0 +1,7 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../../../team/[slug]";
export { default } from "../../../team/[slug]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);

View File

@ -301,6 +301,7 @@
"failed": "Failed",
"password_has_been_reset_login": "Your password has been reset. You can now login with your newly created password.",
"bookerlayout_title": "Layout",
"layout": "Layout",
"bookerlayout_default_title": "Default view",
"bookerlayout_description": "You can select multiple and your bookers can switch views.",
"bookerlayout_user_settings_title": "Booking layout",

View File

@ -300,6 +300,7 @@
"failed": "Échoué",
"password_has_been_reset_login": "Votre mot de passe a été réinitialisé. Vous pouvez désormais vous connecter avec votre nouveau mot de passe.",
"bookerlayout_title": "Mise en page",
"layout": "Mise en page",
"bookerlayout_default_title": "Vue par défaut",
"bookerlayout_description": "Vous pouvez en sélectionner plusieurs, vos utilisateurs peuvent basculer entre les vues.",
"bookerlayout_user_settings_title": "Mise en page de réservation",

View File

@ -299,6 +299,7 @@
"failed": "הפעולה נכשלה",
"password_has_been_reset_login": "הסיסמה שלך אופסה. עכשיו ניתן להיכנס עם הסיסמה החדשה.",
"bookerlayout_title": "פריסה",
"layout": "פריסה",
"bookerlayout_default_title": "תצוגת ברירת מחדל",
"bookerlayout_description": "אתה יכול לבחור כמה והמתזמנים שלך יכולים לשנות תצוגות.",
"bookerlayout_user_settings_title": "פריסת תזמון",

View File

@ -300,6 +300,7 @@
"failed": "Fallito",
"password_has_been_reset_login": "La tua password è stata reimpostata. Ora puoi accedere con la tua password appena creata.",
"bookerlayout_title": "Layout",
"layout": "Layout",
"bookerlayout_default_title": "Vista predefinita",
"bookerlayout_description": "È possibile selezionarne vari e chi fissa gli appuntamenti può cambiare visualizzazione.",
"bookerlayout_user_settings_title": "Layout di programmazione appuntamenti",

View File

@ -11,6 +11,7 @@
"calcom_explained_new_user": "{{appName}} 계정 설정을 완료하세요! 몇 단계만 거치면 모든 일정 문제를 해결할 수 있습니다.",
"have_any_questions": "질문이 있나요? 도와드릴게요.",
"reset_password_subject": "{{appName}}: 비밀번호 재설정 방법",
"email_sent": "이메일이 성공적으로 전송되었습니다",
"event_declined_subject": "거절됨: {{date}} {{title}}",
"event_cancelled_subject": "취소됨: {{date}} {{title}}",
"event_request_declined": "회의 요청이 거절되었습니다.",

View File

@ -299,6 +299,7 @@
"failed": "Neuspešno",
"password_has_been_reset_login": "Vaša lozinka je resetovana. Sada možete da se ulogujete sa vašom novom lozinkom.",
"bookerlayout_title": "Raspored",
"layout": "Raspored",
"bookerlayout_default_title": "Podrazumevani prikaz",
"bookerlayout_description": "Možete da izaberete više njih, a vaši polaznici mogu da menjaju prikaze.",
"bookerlayout_user_settings_title": "Raspored zakazivanja",

View File

@ -290,6 +290,7 @@
"failed": "失败",
"password_has_been_reset_login": "您的密码已重置。您现在可以使用您的新密码登录。",
"bookerlayout_title": "布局",
"layout": "布局",
"bookerlayout_default_title": "默认视图",
"bookerlayout_description": "您可以选择多个布局,这样预约者可以切换视图。",
"bookerlayout_user_settings_title": "预约布局",

View File

@ -136,7 +136,6 @@ export const TeamInviteEmail = (
<>
{props.language("email_no_user_signoff", {
appName: APP_NAME,
entity: props.language(props.isOrg ? "organization" : "team").toLowerCase(),
})}
</>
</p>

View File

@ -150,6 +150,12 @@
data-cal-link="forms/948ae412-d995-4865-875a-48302588de03">
Book through Routing Form [Dark Theme]
</button>
<button data-cal-namespace="popupPaidEvent" data-cal-config='{"layout":"week_view"}' data-cal-link="pro/paid">
Book Paid Event - weekly view
</button>
<button data-cal-namespace="popupPaidEvent" data-cal-config='{"layout":"column_view"}' data-cal-link="pro/paid">
Book Paid Event - column view
</button>
<!-- <div>
<h2>Embed for Pages behind authentication</h2>
<button data-cal-namespace="upcomingBookings" data-cal-config='{"theme":"dark"}' data-cal-link="bookings/upcoming">Show Upcoming Bookings</button>

View File

@ -3,6 +3,8 @@ import type { CSSProperties } from "react";
import { useState, useEffect } from "react";
import embedInit from "@calcom/embed-core/embed-iframe-init";
import type { BookerStore } from "@calcom/features/bookings/Booker/store";
import type { BookerLayouts } from "@calcom/prisma/zod-utils";
import type { Message } from "./embed";
import { sdkActionManager } from "./sdk-event";
@ -16,6 +18,7 @@ export type UiConfig = {
styles?: EmbedStyles & EmbedNonStylesConfig;
//TODO: Extract from tailwind the list of all custom variables and support them in auto-completion as well as runtime validation. Followup with listing all variables in Embed Snippet Generator UI.
cssVarsPerTheme?: Record<Theme, Record<string, string>>;
layout?: BookerLayouts;
};
type SetStyles = React.Dispatch<React.SetStateAction<EmbedStyles>>;
@ -44,6 +47,7 @@ declare global {
__logQueue?: unknown[];
embedStore: typeof embedStore;
applyCssVars: (cssVarsPerTheme: UiConfig["cssVarsPerTheme"]) => void;
setLayout?: BookerStore["setLayout"];
};
CalComPageStatus: string;
isEmbed?: () => boolean;
@ -315,6 +319,10 @@ const methods = {
embedStore.setUiConfig(uiConfig);
}
if (uiConfig.layout) {
window.CalEmbed.setLayout?.(uiConfig.layout);
}
setEmbedStyles(stylesConfig || {});
setEmbedNonStyles(stylesConfig || {});
},

View File

@ -46,7 +46,7 @@ export async function getServerSession(options: {
return cachedSession;
}
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
email: token.email.toLowerCase(),
},

View File

@ -1,6 +1,6 @@
import { LazyMotion, domAnimation, m, AnimatePresence } from "framer-motion";
import dynamic from "next/dynamic";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useMemo } from "react";
import StickyBox from "react-sticky-box";
import { shallow } from "zustand/shallow";
@ -20,6 +20,8 @@ import { extraDaysConfig, fadeInLeft, getBookerSizeClassNames, useBookerResizeAn
import { useBookerStore, useInitializeBookerStore } from "./store";
import type { BookerProps } from "./types";
import { useEvent } from "./utils/event";
import { validateLayout } from "./utils/layout";
import { getQueryParam } from "./utils/query-param";
import { useBrandColors } from "./utils/use-brand-colors";
const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy"));
@ -42,6 +44,9 @@ const BookerComponent = ({
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("rescheduleUid") : null;
const event = useEvent();
const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
if (typeof window !== "undefined") {
window.CalEmbed.setLayout = setLayout;
}
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
const selectedDate = useBookerStore((state) => state.selectedDate);
const [selectedTimeslot, setSelectedTimeslot] = useBookerStore(
@ -52,6 +57,14 @@ const BookerComponent = ({
const extraDays = isTablet ? extraDaysConfig[layout].tablet : extraDaysConfig[layout].desktop;
const bookerLayouts = event.data?.profile?.bookerLayouts || defaultBookerLayoutSettings;
const animationScope = useBookerResizeAnimation(layout, bookerState);
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
// We only want the initial url value, that's why we memo it. The embed seems to change the url, which sometimes drops
// the layout query param.
const layoutFromQueryParam = useMemo(() => validateLayout(getQueryParam("layout") as BookerLayouts), []);
const defaultLayout = isEmbed
? layoutFromQueryParam || BookerLayouts.MONTH_VIEW
: bookerLayouts.defaultLayout;
useBrandColors({
brandColor: event.data?.profile.brandColor,
@ -66,16 +79,16 @@ const BookerComponent = ({
eventId: event?.data?.id,
rescheduleUid,
rescheduleBooking,
layout: bookerLayouts.defaultLayout,
layout: defaultLayout,
});
useEffect(() => {
if (isMobile && layout !== "mobile") {
setLayout("mobile");
} else if (!isMobile && layout === "mobile") {
setLayout(BookerLayouts.MONTH_VIEW);
setLayout(defaultLayout);
}
}, [isMobile, setLayout, layout]);
}, [isMobile, setLayout, layout, defaultLayout]);
useEffect(() => {
if (event.isLoading) return setBookerState("loading");
@ -96,17 +109,21 @@ const BookerComponent = ({
return (
<>
<div className="flex h-full w-full flex-col items-center">
<div className="text-default flex h-full w-full flex-col items-center overflow-x-clip">
<div
ref={animationScope}
className={classNames(
// Sets booker size css variables for the size of all the columns.
...getBookerSizeClassNames(layout, bookerState),
"bg-default dark:bg-muted grid max-w-full items-start overflow-clip dark:[color-scheme:dark] sm:transition-[width] sm:duration-300 sm:motion-reduce:transition-none md:flex-row",
layout === BookerLayouts.MONTH_VIEW && "border-subtle rounded-md border"
"bg-default dark:bg-muted grid max-w-full auto-rows-fr items-start dark:[color-scheme:dark] sm:duration-300 sm:motion-reduce:transition-none md:flex-row",
layout === BookerLayouts.MONTH_VIEW && "rounded-md border",
!isEmbed && "sm:transition-[width] sm:duration-300",
isEmbed && layout === BookerLayouts.MONTH_VIEW && "border-booker sm:border-booker-width",
!isEmbed && layout === BookerLayouts.MONTH_VIEW && "border-subtle",
layout === BookerLayouts.MONTH_VIEW && isEmbed && "mt-20"
)}>
<AnimatePresence>
<BookerSection area="header">
<BookerSection area="header" className={classNames(isMonthView && "fixed top-3 right-3 z-10")}>
<Header
enabledLayouts={bookerLayouts.enabledLayouts}
extraDays={extraDays}

View File

@ -26,6 +26,7 @@ export function Header({
const addToSelectedDate = useBookerStore((state) => state.addToSelectedDate);
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
const selectedDate = dayjs(selectedDateString);
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
const onLayoutToggle = useCallback(
(newLayout: string) => {
@ -35,7 +36,7 @@ export function Header({
[setLayout, layout]
);
if (isMobile || !enabledLayouts) return null;
if (isMobile || isEmbed || !enabledLayouts) return null;
// Only reason we create this component, is because it is used 3 times in this component,
// and this way we can't forget to update one of the props in all places :)
@ -46,11 +47,7 @@ export function Header({
// In month view we only show the layout toggle.
if (isMonthView) {
if (enabledLayouts.length <= 1) return null;
return (
<div className="fixed top-3 right-3 z-10">
<LayoutToggleWithData />
</div>
);
return <LayoutToggleWithData />;
}
return (

View File

@ -142,7 +142,8 @@ export const getBookerSizeClassNames = (layout: BookerLayout, bookerState: Booke
export const useBookerResizeAnimation = (layout: BookerLayout, state: BookerState) => {
const prefersReducedMotion = useReducedMotion();
const [animationScope, animate] = useAnimate();
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
``;
useEffect(() => {
const animationConfig = resizeAnimationConfig[layout][state] || resizeAnimationConfig[layout].default;
@ -163,12 +164,18 @@ export const useBookerResizeAnimation = (layout: BookerLayout, state: BookerStat
minHeight: animationConfig?.minHeight,
};
// We don't animate if users has set prefers-reduced-motion,
// or when the layout is mobile.
if (prefersReducedMotion || layout === "mobile") {
// In this cases we don't animate the booker at all.
if (prefersReducedMotion || layout === "mobile" || isEmbed) {
const styles = { ...nonAnimatedProperties, ...animatedProperties };
Object.keys(styles).forEach((property) => {
animationScope.current.style[property] = styles[property as keyof typeof styles];
if (property === "height") {
// Change 100vh to 100% in embed, since 100vh in iframe will behave weird, because
// the iframe will constantly grow. 100% will simply make sure it grows with the iframe.
animationScope.current.style.height =
animatedProperties.height === "100vh" && isEmbed ? "100%" : animatedProperties.height;
} else {
animationScope.current.style[property] = styles[property as keyof typeof styles];
}
});
} else {
Object.keys(nonAnimatedProperties).forEach((property) => {
@ -180,7 +187,7 @@ export const useBookerResizeAnimation = (layout: BookerLayout, state: BookerStat
ease: cubicBezier(0.4, 0, 0.2, 1),
});
}
}, [animate, animationScope, layout, prefersReducedMotion, state]);
}, [animate, isEmbed, animationScope, layout, prefersReducedMotion, state]);
return animationScope;
};

View File

@ -2,10 +2,11 @@ import { useEffect } from "react";
import { create } from "zustand";
import dayjs from "@calcom/dayjs";
import { BookerLayouts, bookerLayoutOptions } from "@calcom/prisma/zod-utils";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import type { GetBookingType } from "../lib/get-booking";
import type { BookerState, BookerLayout } from "./types";
import { validateLayout } from "./utils/layout";
import { updateQueryParam, getQueryParam, removeQueryParam } from "./utils/query-param";
/**
@ -23,7 +24,7 @@ type StoreInitializeType = {
layout: BookerLayout;
};
type BookerStore = {
export type BookerStore = {
/**
* Event details. These are stored in store for easier
* access in child components.
@ -90,10 +91,6 @@ type BookerStore = {
setFormValues: (values: Record<string, any>) => void;
};
const checkLayout = (layout: BookerLayout) => {
return bookerLayoutOptions.find((validLayout) => validLayout === layout);
};
/**
* The booker store contains the data of the component's
* current state. This data can be reused within child components
@ -104,7 +101,7 @@ const checkLayout = (layout: BookerLayout) => {
export const useBookerStore = create<BookerStore>((set, get) => ({
state: "loading",
setState: (state: BookerState) => set({ state }),
layout: checkLayout(getQueryParam("layout") as BookerLayout) || BookerLayouts.MONTH_VIEW,
layout: validateLayout(getQueryParam("layout") as BookerLayouts) || BookerLayouts.MONTH_VIEW,
setLayout: (layout: BookerLayout) => {
// If we switch to a large layout and don't have a date selected yet,
// we selected it here, so week title is rendered properly.

View File

@ -0,0 +1,6 @@
import type { BookerLayouts } from "@calcom/prisma/zod-utils";
import { bookerLayoutOptions } from "@calcom/prisma/zod-utils";
export const validateLayout = (layout?: BookerLayouts | null) => {
return bookerLayoutOptions.find((validLayout) => validLayout === layout);
};

View File

@ -40,7 +40,7 @@ export const AvailableTimes = ({
const isToday = dayjs().isSame(date, "day");
return (
<div className={classNames("text-default", className)}>
<div className={classNames("text-default flex flex-col", className)}>
<header className="bg-default before:bg-default dark:bg-muted dark:before:bg-muted mb-3 flex w-full flex-row items-center font-medium">
<span
className={classNames(

View File

@ -11,7 +11,13 @@ import { useFilterQuery } from "../lib/useFilterQuery";
export const TeamsMemberFilter = () => {
const { t } = useLocale();
const session = useSession();
const { data: query, pushItemToKey, removeItemByKeyAndValue, removeByKey, removeAllQueryParams } = useFilterQuery();
const {
data: query,
pushItemToKey,
removeItemByKeyAndValue,
removeByKey,
removeAllQueryParams,
} = useFilterQuery();
const { data } = trpc.viewer.teams.list.useQuery();
const [dropdownTitle, setDropdownTitle] = useState<string>(t("all_bookings_filter_label"));

View File

@ -2,6 +2,7 @@ import { Fragment } from "react";
import React from "react";
import classNames from "@calcom/lib/classNames";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Clock, CheckSquare, RefreshCcw, CreditCard } from "@calcom/ui/components/icon";
@ -145,7 +146,8 @@ export const EventDetails = ({ event, blocks = defaultEventDetailsBlocks }: Even
);
case EventDetailBlocks.PRICE:
if (event.price === 0) return null;
const paymentAppData = getPaymentAppData(event);
if (event.price <= 0 || paymentAppData.price <= 0) return null;
return (
<EventMetaBlock key={block} icon={CreditCard}>

View File

@ -18,7 +18,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const {
data: { org: slug },
} = parsedQuery;
if (!slug) if (!slug) return res.status(400).json({ message: "Org is needed" });
if (!slug) return res.status(400).json({ message: "Org is needed" });
const org = await prisma.team.findFirst({ where: { slug }, select: { children: true, metadata: true } });

View File

@ -11,22 +11,31 @@ export const appHostnames = [
"localhost:3000",
];
/**
* return the org slug
* @param hostname
*/
export function getOrgDomain(hostname: string) {
// Find which hostname is being currently used
const currentHostname = appHostnames.find((ahn) => {
const url = new URL(WEBAPP_URL);
const hostname = `${url.hostname}${url.port ? `:${url.port}` : ""}`;
return hostname.endsWith(`.${ahn}`);
const testHostname = `${url.hostname}${url.port ? `:${url.port}` : ""}`;
return testHostname.endsWith(`.${ahn}`);
});
// Define which is the current domain/subdomain
return hostname.replace(`.${currentHostname}` ?? "", "");
if (currentHostname) {
// Define which is the current domain/subdomain
const slug = hostname.replace(`.${currentHostname}` ?? "", "");
return slug.indexOf(".") === -1 ? slug : null;
}
return null;
}
export function orgDomainConfig(hostname: string) {
const currentOrgDomain = getOrgDomain(hostname);
return {
currentOrgDomain,
isValidOrgDomain: currentOrgDomain !== "app" && !appHostnames.includes(currentOrgDomain),
isValidOrgDomain:
currentOrgDomain && currentOrgDomain !== "app" && !appHostnames.includes(currentOrgDomain),
};
}

View File

@ -31,7 +31,7 @@ export const isSAMLAdmin = (email: string) => {
};
export const samlTenantProduct = async (prisma: PrismaClient, email: string) => {
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
email,
},

View File

@ -192,7 +192,20 @@ function getProfileFromEvent(event: Event) {
if (!profile) throw new Error("Event has no owner");
const username = "username" in profile ? profile.username : team?.slug;
if (!username) throw new Error("Event has no username/team slug");
if (!username) {
if (event.slug === "test") {
// @TODO: This is a temporary debug statement that should be removed asap.
throw new Error(
"Ciaran event error" +
JSON.stringify(team) +
" -- " +
JSON.stringify(hosts) +
" -- " +
JSON.stringify(owner)
);
}
throw new Error("Event has no username/team slug");
}
const weekStart = hosts?.[0]?.user?.weekStart || owner?.weekStart || "Monday";
const basePath = team ? `/team/${username}` : `/${username}`;
const eventMetaData = EventTypeMetaDataSchema.parse(event.metadata || {});

View File

@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest";
import { orgDomainConfig, getOrgDomain } from "@calcom/features/ee/organizations/lib/orgDomains";
import * as constants from "@calcom/lib/constants";
describe("Org Domains Utils", () => {
describe("orgDomainConfig", () => {
it("should return a valid org domain", () => {
Object.defineProperty(constants, 'WEBAPP_URL', {value:"https://app.cal.com"});
expect(orgDomainConfig("acme.cal.com")).toEqual({
currentOrgDomain: "acme",
isValidOrgDomain: true
});
});
it("should return a non valid org domain", () => {
Object.defineProperty(constants, 'WEBAPP_URL', {value:"https://app.cal.com"});
expect(orgDomainConfig("app.cal.com")).toEqual({
currentOrgDomain: "app",
isValidOrgDomain: false
});
});
});
describe("getOrgDomain", () => {
it("should handle a prod web app url with a prod subdomain hostname", () => {
Object.defineProperty(constants, 'WEBAPP_URL', {value:"https://app.cal.com"});
expect(getOrgDomain("acme.cal.com")).toEqual("acme");
});
it("should handle a prod web app url with a staging subdomain hostname", () => {
Object.defineProperty(constants, 'WEBAPP_URL', {value:"https://app.cal.com"});
expect(getOrgDomain("acme.cal.dev")).toEqual(null);
});
it("should handle a local web app with port url with a local subdomain hostname", () => {
Object.defineProperty(constants, 'WEBAPP_URL', {value:"http://app.cal.local:3000"});
expect(getOrgDomain("acme.cal.local:3000")).toEqual("acme");
});
it("should handle a local web app with port url with a non-local subdomain hostname", () => {
Object.defineProperty(constants, 'WEBAPP_URL', {value:"http://app.cal.local:3000"});
expect(getOrgDomain("acme.cal.com:3000")).toEqual(null);
});
})
});

View File

@ -337,7 +337,7 @@ export default abstract class BaseCalendarService implements Calendar {
const userTimeZone = userId ? await this.getUserTimezoneFromDB(userId) : "Europe/London";
const events: { start: string; end: string }[] = [];
objects.forEach((object) => {
if (object.data == null || JSON.stringify(object.data) == "{}") return;
if (!object || object.data == null || JSON.stringify(object.data) == "{}") return;
let vcalendar: ICAL.Component;
try {
const jcalData = ICAL.parse(sanitizeCalendarObject(object));

View File

@ -8,7 +8,7 @@ export const WEBAPP_URL =
RAILWAY_STATIC_URL ||
HEROKU_URL ||
RENDER_URL ||
"http:/localhost:3000";
"http://localhost:3000";
/** @deprecated use `WEBAPP_URL` */
export const BASE_URL = WEBAPP_URL;
export const WEBSITE_URL = process.env.NEXT_PUBLIC_WEBSITE_URL || "https://cal.com";

View File

@ -20,7 +20,7 @@ type DeleteMeOptions = {
export const deleteMeHandler = async ({ ctx, input }: DeleteMeOptions) => {
// Check if input.password is correct
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
email: ctx.user.email.toLowerCase(),
},

View File

@ -12,7 +12,7 @@ type DeleteMeWithoutPasswordOptions = {
};
export const deleteMeWithoutPasswordHandler = async ({ ctx }: DeleteMeWithoutPasswordOptions) => {
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
email: ctx.user.email.toLowerCase(),
},

View File

@ -50,7 +50,7 @@ const vercelCreateDomain = async (domain: string) => {
export const createHandler = async ({ input }: CreateOptions) => {
const { slug, name, adminEmail, adminUsername, check } = input;
const userCollisions = await prisma.user.findFirst({
const userCollisions = await prisma.user.findUnique({
where: {
email: adminEmail,
},

View File

@ -65,7 +65,7 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
userId: ctx.user.id,
},
{
teamId: userEventType.teamId,
teamId: userEventType.teamId || undefined,
},
],
},