Merge branch 'feat/organizations' into feat/organizations-banner
This commit is contained in:
commit
952dde9f54
|
@ -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>
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" }],
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -0,0 +1,7 @@
|
|||
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||
|
||||
import { getServerSideProps as _getServerSideProps } from "../../[user]";
|
||||
|
||||
export { default } from "../../[user]";
|
||||
|
||||
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -299,6 +299,7 @@
|
|||
"failed": "הפעולה נכשלה",
|
||||
"password_has_been_reset_login": "הסיסמה שלך אופסה. עכשיו ניתן להיכנס עם הסיסמה החדשה.",
|
||||
"bookerlayout_title": "פריסה",
|
||||
"layout": "פריסה",
|
||||
"bookerlayout_default_title": "תצוגת ברירת מחדל",
|
||||
"bookerlayout_description": "אתה יכול לבחור כמה והמתזמנים שלך יכולים לשנות תצוגות.",
|
||||
"bookerlayout_user_settings_title": "פריסת תזמון",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "회의 요청이 거절되었습니다.",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -290,6 +290,7 @@
|
|||
"failed": "失败",
|
||||
"password_has_been_reset_login": "您的密码已重置。您现在可以使用您的新密码登录。",
|
||||
"bookerlayout_title": "布局",
|
||||
"layout": "布局",
|
||||
"bookerlayout_default_title": "默认视图",
|
||||
"bookerlayout_description": "您可以选择多个布局,这样预约者可以切换视图。",
|
||||
"bookerlayout_user_settings_title": "预约布局",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 || {});
|
||||
},
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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(
|
||||
|
|
|
@ -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"));
|
||||
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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 } });
|
||||
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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 || {});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
})
|
||||
});
|
|
@ -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));
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -65,7 +65,7 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
|
|||
userId: ctx.user.id,
|
||||
},
|
||||
{
|
||||
teamId: userEventType.teamId,
|
||||
teamId: userEventType.teamId || undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue
Block a user