Merge branch 'main' into fix/organizer-location

fix/organizer-location
Hariom Balhara 2023-10-26 08:29:54 +05:30 committed by GitHub
commit 38638f99f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 674 additions and 31 deletions

View File

@ -0,0 +1,15 @@
diff --git a/index.cjs b/index.cjs
index b645707a3549fc298508726e404243499bbed499..f34b0891e99b275a9218e253f303f43d31ef3f73 100644
--- a/index.cjs
+++ b/index.cjs
@@ -13,8 +13,8 @@ function withMetadataArgument(func, _arguments) {
// https://github.com/babel/babel/issues/2212#issuecomment-131827986
// An alternative approach:
// https://www.npmjs.com/package/babel-plugin-add-module-exports
-exports = module.exports = min.parsePhoneNumberFromString
-exports['default'] = min.parsePhoneNumberFromString
+// exports = module.exports = min.parsePhoneNumberFromString
+// exports['default'] = min.parsePhoneNumberFromString
// `parsePhoneNumberFromString()` named export is now considered legacy:
// it has been promoted to a default export due to being too verbose.

View File

@ -0,0 +1,26 @@
diff --git a/dist/commonjs/serverSideTranslations.js b/dist/commonjs/serverSideTranslations.js
index bcad3d02fbdfab8dacb1d85efd79e98623a0c257..fff668f598154a13c4030d1b4a90d5d9c18214ad 100644
--- a/dist/commonjs/serverSideTranslations.js
+++ b/dist/commonjs/serverSideTranslations.js
@@ -36,7 +36,6 @@ var _fs = _interopRequireDefault(require("fs"));
var _path = _interopRequireDefault(require("path"));
var _createConfig = require("./config/createConfig");
var _node = _interopRequireDefault(require("./createClient/node"));
-var _appWithTranslation = require("./appWithTranslation");
var _utils = require("./utils");
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
@@ -110,12 +109,8 @@ var serverSideTranslations = /*#__PURE__*/function () {
lng: initialLocale
}));
localeExtension = config.localeExtension, localePath = config.localePath, fallbackLng = config.fallbackLng, reloadOnPrerender = config.reloadOnPrerender;
- if (!reloadOnPrerender) {
- _context.next = 18;
- break;
- }
_context.next = 18;
- return _appWithTranslation.globalI18n === null || _appWithTranslation.globalI18n === void 0 ? void 0 : _appWithTranslation.globalI18n.reloadResources();
+ return void 0;
case 18:
_createClient = (0, _node["default"])(_objectSpread(_objectSpread({}, config), {}, {
lng: initialLocale

109
apps/web/app/layout.tsx Normal file
View File

@ -0,0 +1,109 @@
import type { Metadata } from "next";
import { headers as nextHeaders, cookies as nextCookies } from "next/headers";
import Script from "next/script";
import React from "react";
import { getLocale } from "@calcom/features/auth/lib/getLocale";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import "../styles/globals.css";
export const metadata: Metadata = {
icons: {
icon: [
{
sizes: "32x32",
url: "/api/logo?type=favicon-32",
},
{
sizes: "16x16",
url: "/api/logo?type=favicon-16",
},
],
apple: {
sizes: "180x180",
url: "/api/logo?type=apple-touch-icon",
},
other: [
{
url: "/safari-pinned-tab.svg",
rel: "mask-icon",
},
],
},
manifest: "/site.webmanifest",
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#f9fafb" },
{ media: "(prefers-color-scheme: dark)", color: "#1C1C1C" },
],
other: {
"msapplication-TileColor": "#000000",
},
};
const getInitialProps = async (
url: string,
headers: ReturnType<typeof nextHeaders>,
cookies: ReturnType<typeof nextCookies>
) => {
const { pathname, searchParams } = new URL(url);
const isEmbed = pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null;
const embedColorScheme = searchParams?.get("ui.color-scheme");
// @ts-expect-error we cannot access ctx.req in app dir, however headers and cookies are only properties needed to extract the locale
const newLocale = await getLocale({ headers, cookies });
let direction = "ltr";
try {
const intlLocale = new Intl.Locale(newLocale);
// @ts-expect-error INFO: Typescript does not know about the Intl.Locale textInfo attribute
direction = intlLocale.textInfo?.direction;
} catch (e) {
console.error(e);
}
return { isEmbed, embedColorScheme, locale: newLocale, direction };
};
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const headers = nextHeaders();
const cookies = nextCookies();
const fullUrl = headers.get("x-url") ?? "";
const nonce = headers.get("x-csp") ?? "";
const { locale, direction, isEmbed, embedColorScheme } = await getInitialProps(fullUrl, headers, cookies);
return (
<html
lang={locale}
dir={direction}
style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}>
<head nonce={nonce}>
{!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && (
// eslint-disable-next-line @next/next/no-sync-scripts
<Script
data-project-id="KjpMrKTnXquJVKfeqmjdTffVPf1a6Unw2LZ58iE4"
src="https://snippet.meticulous.ai/v1/stagingMeticulousSnippet.js"
/>
)}
</head>
<body
className="dark:bg-darkgray-50 desktop-transparent bg-subtle antialiased"
style={
isEmbed
? {
background: "transparent",
// Keep the embed hidden till parent initializes and
// - gives it the appropriate styles if UI instruction is there.
// - gives iframe the appropriate height(equal to document height) which can only be known after loading the page once in browser.
// - Tells iframe which mode it should be in (dark/light) - if there is a a UI instruction for that
visibility: "hidden",
}
: {}
}>
{children}
</body>
</html>
);
}

View File

@ -0,0 +1,88 @@
"use client";
import type { SSRConfig } from "next-i18next";
import { Inter } from "next/font/google";
import localFont from "next/font/local";
// import I18nLanguageHandler from "@components/I18nLanguageHandler";
import { usePathname } from "next/navigation";
import Script from "next/script";
import type { ReactNode } from "react";
import "@calcom/embed-core/src/embed-iframe";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { trpc } from "@calcom/trpc/react";
import type { AppProps } from "@lib/app-providers-app-dir";
import AppProviders from "@lib/app-providers-app-dir";
export interface CalPageWrapper {
(props?: AppProps): JSX.Element;
PageWrapper?: AppProps["Component"]["PageWrapper"];
}
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
const calFont = localFont({
src: "../fonts/CalSans-SemiBold.woff2",
variable: "--font-cal",
preload: true,
display: "swap",
});
export type PageWrapperProps = Readonly<{
getLayout: (page: React.ReactElement) => ReactNode;
children: React.ReactElement;
requiresLicense: boolean;
isThemeSupported: boolean;
isBookingPage: boolean;
nonce: string | undefined;
themeBasis: string | null;
i18n?: SSRConfig;
}>;
function PageWrapper(props: PageWrapperProps) {
const pathname = usePathname();
let pageStatus = "200";
if (pathname === "/404") {
pageStatus = "404";
} else if (pathname === "/500") {
pageStatus = "500";
}
// On client side don't let nonce creep into DOM
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
// See https://github.com/kentcdodds/nonce-hydration-issues
// Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
const nonce = typeof window !== "undefined" ? (props.nonce ? "" : undefined) : props.nonce;
const providerProps: PageWrapperProps = {
...props,
nonce,
};
const getLayout: (page: React.ReactElement) => ReactNode = props.getLayout ?? ((page) => page);
return (
<AppProviders {...providerProps}>
{/* <I18nLanguageHandler locales={props.router.locales || []} /> */}
<>
<Script
nonce={nonce}
id="page-status"
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
/>
<style jsx global>{`
:root {
--font-inter: ${interFont.style.fontFamily};
--font-cal: ${calFont.style.fontFamily};
}
`}</style>
{getLayout(
props.requiresLicense ? <LicenseRequired>{props.children}</LicenseRequired> : props.children
)}
</>
</AppProviders>
);
}
export default trpc.withTRPC(PageWrapper);

View File

@ -433,6 +433,23 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
</>
)}
/>
<Controller
name="lockTimeZoneToggleOnBookingPage"
control={formMethods.control}
defaultValue={eventType.lockTimeZoneToggleOnBookingPage}
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("lock_timezone_toggle_on_booking_page")}
{...shouldLockDisableProps("lockTimeZoneToggleOnBookingPage")}
description={t("description_lock_timezone_toggle_on_booking_page")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
)}
/>
{allowDisablingAttendeeConfirmationEmails(workflows) && (
<>
<Controller

View File

@ -0,0 +1,291 @@
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { dir } from "i18next";
import type { Session } from "next-auth";
import { SessionProvider, useSession } from "next-auth/react";
import { EventCollectionProvider } from "next-collect/client";
import { appWithTranslation, type SSRConfig } from "next-i18next";
import { ThemeProvider } from "next-themes";
import type { AppProps as NextAppProps } from "next/app";
import type { ReadonlyURLSearchParams } from "next/navigation";
import { useSearchParams } from "next/navigation";
import { useEffect, type ReactNode } from "react";
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
import { FeatureProvider } from "@calcom/features/flags/context/provider";
import { useFlags } from "@calcom/features/flags/hooks";
import { MetaProvider } from "@calcom/ui";
import useIsBookingPage from "@lib/hooks/useIsBookingPage";
import type { WithNonceProps } from "@lib/withNonce";
import { useViewerI18n } from "@components/I18nLanguageHandler";
import type { PageWrapperProps } from "@components/PageWrapperAppDir";
// Workaround for https://github.com/vercel/next.js/issues/8592
export type AppProps = Omit<
NextAppProps<
WithNonceProps<{
themeBasis?: string;
session: Session;
}>
>,
"Component"
> & {
Component: NextAppProps["Component"] & {
requiresLicense?: boolean;
isThemeSupported?: boolean;
isBookingPage?: boolean | ((arg: { router: NextAppProps["router"] }) => boolean);
getLayout?: (page: React.ReactElement) => ReactNode;
PageWrapper?: (props: AppProps) => JSX.Element;
};
/** Will be defined only is there was an error */
err?: Error;
};
const getEmbedNamespace = (searchParams: ReadonlyURLSearchParams) => {
// Mostly embed query param should be available on server. Use that there.
// Use the most reliable detection on client
return typeof window !== "undefined" ? window.getEmbedNamespace() : searchParams.get("embed") ?? null;
};
// @ts-expect-error appWithTranslation expects AppProps
const AppWithTranslationHoc = appWithTranslation(({ children }) => <>{children}</>);
const CustomI18nextProvider = (props: { children: React.ReactElement; i18n?: SSRConfig }) => {
/**
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
**/
// @TODO
const session = useSession();
const locale =
session?.data?.user.locale ?? typeof window !== "undefined" ? window.document.documentElement.lang : "en";
useEffect(() => {
try {
// @ts-expect-error TS2790: The operand of a 'delete' operator must be optional.
delete window.document.documentElement["lang"];
window.document.documentElement.lang = locale;
// Next.js writes the locale to the same attribute
// https://github.com/vercel/next.js/blob/1609da2d9552fed48ab45969bdc5631230c6d356/packages/next/src/shared/lib/router/router.ts#L1786
// which can result in a race condition
// this property descriptor ensures this never happens
Object.defineProperty(window.document.documentElement, "lang", {
configurable: true,
// value: locale,
set: function (this) {
// empty setter on purpose
},
get: function () {
return locale;
},
});
} catch (error) {
console.error(error);
window.document.documentElement.lang = locale;
}
window.document.dir = dir(locale);
}, [locale]);
const clientViewerI18n = useViewerI18n(locale);
const i18n = clientViewerI18n.data?.i18n ?? props.i18n;
if (!i18n || !i18n._nextI18Next) {
return null;
}
return (
// @ts-expect-error AppWithTranslationHoc expects AppProps
<AppWithTranslationHoc pageProps={{ _nextI18Next: i18n._nextI18Next }}>
{props.children}
</AppWithTranslationHoc>
);
};
const enum ThemeSupport {
// e.g. Login Page
None = "none",
// Entire App except Booking Pages
App = "systemOnly",
// Booking Pages(including Routing Forms)
Booking = "userConfigured",
}
type CalcomThemeProps = Readonly<{
isBookingPage: boolean;
themeBasis: string | null;
nonce: string | undefined;
isThemeSupported: boolean;
children: React.ReactNode;
}>;
const CalcomThemeProvider = (props: CalcomThemeProps) => {
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
// One such example is our Embeds Demo and Testing page at http://localhost:3100
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
const searchParams = useSearchParams();
const embedNamespace = searchParams ? getEmbedNamespace(searchParams) : null;
const isEmbedMode = typeof embedNamespace === "string";
return (
<ThemeProvider {...getThemeProviderProps({ ...props, isEmbedMode, embedNamespace })}>
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
{typeof window !== "undefined" && !isEmbedMode && (
<style jsx global>
{`
.dark {
color-scheme: dark;
}
`}
</style>
)}
{props.children}
</ThemeProvider>
);
};
/**
* The most important job for this fn is to generate correct storageKey for theme persistenc.
* `storageKey` is important because that key is listened for changes(using [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event) and any pages opened will change it's theme based on that(as part of next-themes implementation).
* Choosing the right storageKey avoids theme flickering caused by another page using different theme
* So, we handle all the cases here namely,
* - Both Booking Pages, /free/30min and /pro/30min but configured with different themes but being operated together.
* - Embeds using different namespace. They can be completely themed different on the same page.
* - Embeds using the same namespace but showing different cal.com links with different themes
* - Embeds using the same namespace and showing same cal.com links with different themes(Different theme is possible for same cal.com link in case of embed because of theme config available in embed)
* - App has different theme then Booking Pages.
*
* All the above cases have one thing in common, which is the origin and thus localStorage is shared and thus `storageKey` is critical to avoid theme flickering.
*
* Some things to note:
* - There is a side effect of so many factors in `storageKey` that many localStorage keys will be created if a user goes through all these scenarios(e.g like booking a lot of different users)
* - Some might recommend disabling localStorage persistence but that doesn't give good UX as then we would default to light theme always for a few seconds before switching to dark theme(if that's the user's preference).
* - We can't disable [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event handling as well because changing theme in one tab won't change the theme without refresh in other tabs. That's again a bad UX
* - Theme flickering becomes infinitely ongoing in case of embeds because of the browser's delay in processing `storage` event within iframes. Consider two embeds simulatenously opened with pages A and B. Note the timeline and keep in mind that it happened
* because 'setItem(A)' and 'Receives storageEvent(A)' allowed executing setItem(B) in b/w because of the delay.
* - t1 -> setItem(A) & Fires storageEvent(A) - On Page A) - Current State(A)
* - t2 -> setItem(B) & Fires storageEvent(B) - On Page B) - Current State(B)
* - t3 -> Receives storageEvent(A) & thus setItem(A) & thus fires storageEvent(A) (On Page B) - Current State(A)
* - t4 -> Receives storageEvent(B) & thus setItem(B) & thus fires storageEvent(B) (On Page A) - Current State(B)
* - ... and so on ...
*/
function getThemeProviderProps(props: {
isBookingPage: boolean;
themeBasis: string | null;
nonce: string | undefined;
isEmbedMode: boolean;
embedNamespace: string | null;
isThemeSupported: boolean;
}) {
const themeSupport = props.isBookingPage
? ThemeSupport.Booking
: // if isThemeSupported is explicitly false, we don't use theme there
props.isThemeSupported === false
? ThemeSupport.None
: ThemeSupport.App;
const isBookingPageThemeSupportRequired = themeSupport === ThemeSupport.Booking;
if ((isBookingPageThemeSupportRequired || props.isEmbedMode) && !props.themeBasis) {
console.warn(
"`themeBasis` is required for booking page theme support. Not providing it will cause theme flicker."
);
}
const appearanceIdSuffix = props.themeBasis ? `:${props.themeBasis}` : "";
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
let embedExplicitlySetThemeSuffix = "";
if (typeof window !== "undefined") {
const embedTheme = window.getEmbedTheme();
if (embedTheme) {
embedExplicitlySetThemeSuffix = `:${embedTheme}`;
}
}
const storageKey = props.isEmbedMode
? // Same Namespace, Same Organizer but different themes would still work seamless and not cause theme flicker
// Even though it's recommended to use different namespaces when you want to theme differently on the same page but if the embeds are on different pages, the problem can still arise
`embed-theme-${props.embedNamespace}${appearanceIdSuffix}${embedExplicitlySetThemeSuffix}`
: themeSupport === ThemeSupport.App
? "app-theme"
: isBookingPageThemeSupportRequired
? `booking-theme${appearanceIdSuffix}`
: undefined;
return {
storageKey,
forcedTheme,
themeSupport,
nonce: props.nonce,
enableColorScheme: false,
enableSystem: themeSupport !== ThemeSupport.None,
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
// This is how login to dashboard soft navigation changes theme from light to dark
key: storageKey,
attribute: "class",
};
}
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
const flags = useFlags();
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
}
function useOrgBrandingValues() {
const session = useSession();
return session?.data?.user.org;
}
function OrgBrandProvider({ children }: { children: React.ReactNode }) {
const orgBrand = useOrgBrandingValues();
return <OrgBrandingProvider value={{ orgBrand }}>{children}</OrgBrandingProvider>;
}
const AppProviders = (props: PageWrapperProps) => {
// No need to have intercom on public pages - Good for Page Performance
const isBookingPage = useIsBookingPage();
const RemainingProviders = (
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
<SessionProvider>
<CustomI18nextProvider i18n={props.i18n}>
<TooltipProvider>
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
<CalcomThemeProvider
themeBasis={props.themeBasis}
nonce={props.nonce}
isThemeSupported={props.isThemeSupported}
isBookingPage={props.isBookingPage || isBookingPage}>
<FeatureFlagsProvider>
<OrgBrandProvider>
<MetaProvider>{props.children}</MetaProvider>
</OrgBrandProvider>
</FeatureFlagsProvider>
</CalcomThemeProvider>
</TooltipProvider>
</CustomI18nextProvider>
</SessionProvider>
</EventCollectionProvider>
);
if (isBookingPage) {
return RemainingProviders;
}
return (
<DynamicHelpscoutProvider>
<DynamicIntercomProvider>{RemainingProviders}</DynamicIntercomProvider>
</DynamicHelpscoutProvider>
);
};
export default AppProviders;

View File

@ -226,6 +226,14 @@ const nextConfig = {
},
async rewrites() {
const beforeFiles = [
{
/**
* Needed due to the introduction of dotted usernames
* @see https://github.com/calcom/cal.com/pull/11706
*/
source: "/embed.js",
destination: "/embed/embed.js",
},
{
source: "/login",
destination: "/auth/login",

View File

@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.4.3",
"version": "3.4.4",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",

View File

@ -264,6 +264,7 @@ export type UserPageProps = {
| "slug"
| "length"
| "hidden"
| "lockTimeZoneToggleOnBookingPage"
| "requiresConfirmation"
| "requiresBookerEmailVerification"
| "price"

View File

@ -87,6 +87,7 @@ export type FormValues = {
offsetStart: number;
description: string;
disableGuests: boolean;
lockTimeZoneToggleOnBookingPage: boolean;
requiresConfirmation: boolean;
requiresBookerEmailVerification: boolean;
recurringEvent: RecurringEvent | null;

View File

@ -150,14 +150,14 @@ test.describe("unauthorized user sees correct translations (pt)", async () => {
test.describe("unauthorized user sees correct translations (pt-br)", async () => {
test.use({
locale: "pt-br",
locale: "pt-BR",
});
test("should use correct translations and html attributes", async ({ page }) => {
await page.goto("/");
await page.waitForLoadState("load");
await page.locator("html[lang=pt-br]").waitFor({ state: "attached" });
await page.locator("html[lang=pt-BR]").waitFor({ state: "attached" });
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
{
@ -181,7 +181,8 @@ test.describe("unauthorized user sees correct translations (es-419)", async () =
await page.goto("/");
await page.waitForLoadState("load");
await page.locator("html[lang=es-419]").waitFor({ state: "attached" });
// es-419 is disabled in i18n config, so es should be used as fallback
await page.locator("html[lang=es]").waitFor({ state: "attached" });
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
{

View File

@ -2094,5 +2094,7 @@
"overlay_my_calendar":"Overlay my calendar",
"overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.",
"view_overlay_calendar_events":"View your calendar events to prevent clashed booking.",
"lock_timezone_toggle_on_booking_page": "Lock timezone on booking page",
"description_lock_timezone_toggle_on_booking_page" : "To lock the timezone on booking page, useful for in-person events.",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -37,8 +37,14 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
},
});
// always preload i18n
await ssg.viewer.public.i18n.fetch({ locale, CalComVersion: CALCOM_VERSION });
// i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch
// we can set query data directly to the queryClient
const queryKey = [
["viewer", "public", "i18n"],
{ input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" },
];
ssg.queryClient.setQueryData(queryKey, { i18n: _i18n });
return ssg;
}

View File

@ -25,11 +25,17 @@ export async function ssrInit(context: GetServerSidePropsContext, options?: { no
ctx: { ...ctx, locale, i18n },
});
// i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch
// we can set query data directly to the queryClient
const queryKey = [
["viewer", "public", "i18n"],
{ input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" },
];
if (!options?.noI18nPreload) {
ssr.queryClient.setQueryData(queryKey, { i18n });
}
await Promise.allSettled([
// always preload "viewer.public.i18n"
!options?.noI18nPreload
? ssr.viewer.public.i18n.prefetch({ locale, CalComVersion: CALCOM_VERSION })
: Promise.resolve({}),
// So feature flags are available on first render
ssr.viewer.features.map.prefetch(),
// Provides a better UX to the users who have already upgraded.

View File

@ -98,12 +98,19 @@ describe("handleChildrenEventTypes", () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line
const { schedulingType, id, teamId, timeZone, users, requiresBookerEmailVerification, ...evType } =
mockFindFirstEventType({
id: 123,
metadata: { managedEventConfig: {} },
locations: [],
});
const {
schedulingType,
id,
teamId,
timeZone,
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
...evType
} = mockFindFirstEventType({
id: 123,
metadata: { managedEventConfig: {} },
locations: [],
});
const result = await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [], team: { name: "" } },
@ -145,6 +152,7 @@ describe("handleChildrenEventTypes", () => {
userId,
scheduleId,
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },
@ -230,12 +238,19 @@ describe("handleChildrenEventTypes", () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line
const { schedulingType, id, teamId, timeZone, users, requiresBookerEmailVerification, ...evType } =
mockFindFirstEventType({
id: 123,
metadata: { managedEventConfig: {} },
locations: [],
});
const {
schedulingType,
id,
teamId,
timeZone,
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
...evType
} = mockFindFirstEventType({
id: 123,
metadata: { managedEventConfig: {} },
locations: [],
});
prismaMock.eventType.deleteMany.mockResolvedValue([123] as unknown as Prisma.BatchPayload);
const result = await updateChildrenEventTypes({
eventTypeId: 1,
@ -277,6 +292,7 @@ describe("handleChildrenEventTypes", () => {
parentId,
userId,
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },
@ -327,6 +343,7 @@ describe("handleChildrenEventTypes", () => {
userId: _userId,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
requiresBookerEmailVerification,
lockTimeZoneToggleOnBookingPage,
...evType
} = mockFindFirstEventType({
metadata: { managedEventConfig: {} },

View File

@ -107,7 +107,9 @@
"@apidevtools/json-schema-ref-parser": "9.0.9",
"@types/node": "16.9.1",
"@types/react": "18.0.26",
"@types/react-dom": "^18.0.9"
"@types/react-dom": "^18.0.9",
"libphonenumber-js@^1.10.12": "patch:libphonenumber-js@npm%3A1.10.12#./.yarn/patches/libphonenumber-js-npm-1.10.12-51c84f8bf1.patch",
"next-i18next@^13.2.2": "patch:next-i18next@npm%3A13.3.0#./.yarn/patches/next-i18next-npm-13.3.0-bf25b0943c.patch"
},
"lint-staged": {
"(apps|packages)/**/*.{js,ts,jsx,tsx}": [

View File

@ -1,7 +1,11 @@
import { parse } from "accept-language-parser";
import { lookup } from "bcp-47-match";
import type { GetTokenParams } from "next-auth/jwt";
import { getToken } from "next-auth/jwt";
//@ts-expect-error no type definitions
import { i18n } from "@calcom/web/next-i18next.config";
/**
* This is a slimmed down version of the `getServerSession` function from
* `next-auth`.
@ -40,5 +44,9 @@ export const getLocale = async (req: GetTokenParams["req"]): Promise<string> =>
// the regex underneath is more permissive
const testedRegion = /^[a-zA-Z0-9]+$/.test(region) ? region : "";
return `${testedCode}${testedRegion !== "" ? "-" : ""}${testedRegion}`;
const requestedLocale = `${testedCode}${testedRegion !== "" ? "-" : ""}${testedRegion}`;
// use fallback to closest supported locale.
// for instance, es-419 will be transformed to es
return lookup(i18n.locales, requestedLocale) ?? requestedLocale;
};

View File

@ -105,6 +105,7 @@ export const EventMeta = () => {
</EventMetaBlock>
)}
<EventDetails event={event} />
<EventMetaBlock
className="cursor-pointer [&_.current-timezone:before]:focus-within:opacity-100 [&_.current-timezone:before]:hover:opacity-100"
contentClassName="relative max-w-[90%]"
@ -112,7 +113,10 @@ export const EventMeta = () => {
{bookerState === "booking" ? (
<>{timezone}</>
) : (
<span className="min-w-32 current-timezone before:bg-subtle -mt-[2px] flex h-6 max-w-full items-center justify-start before:absolute before:inset-0 before:bottom-[-3px] before:left-[-30px] before:top-[-3px] before:w-[calc(100%_+_35px)] before:rounded-md before:py-3 before:opacity-0 before:transition-opacity">
<span
className={`min-w-32 current-timezone before:bg-subtle -mt-[2px] flex h-6 max-w-full items-center justify-start before:absolute before:inset-0 before:bottom-[-3px] before:left-[-30px] before:top-[-3px] before:w-[calc(100%_+_35px)] before:rounded-md before:py-3 before:opacity-0 before:transition-opacity ${
event.lockTimeZoneToggleOnBookingPage ? "cursor-not-allowed" : ""
}`}>
<TimezoneSelect
menuPosition="fixed"
classNames={{
@ -124,6 +128,7 @@ export const EventMeta = () => {
}}
value={timezone}
onChange={(tz) => setTimezone(tz.value)}
isDisabled={event.lockTimeZoneToggleOnBookingPage}
/>
</span>
)}

View File

@ -276,6 +276,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
periodEndDate: true,
periodDays: true,
periodCountCalendarDays: true,
lockTimeZoneToggleOnBookingPage: true,
requiresConfirmation: true,
requiresBookerEmailVerification: true,
userId: true,
@ -1851,6 +1852,7 @@ async function handler(
...eventTypeInfo,
uid: resultBooking?.uid || uid,
bookingId: booking?.id,
rescheduleId: originalRescheduledBooking?.id || undefined,
rescheduleUid,
rescheduleStartTime: originalRescheduledBooking?.startTime
? dayjs(originalRescheduledBooking?.startTime).utc().format()
@ -2377,8 +2379,8 @@ async function handler(
...evt,
...eventTypeInfo,
bookingId: booking?.id,
rescheduleId: originalRescheduledBooking?.id || undefined,
rescheduleUid,
oldBookingId: originalRescheduledBooking?.id || undefined,
rescheduleStartTime: originalRescheduledBooking?.startTime
? dayjs(originalRescheduledBooking?.startTime).utc().format()
: undefined,
@ -2685,6 +2687,7 @@ const findBookingQuery = async (bookingId: number) => {
description: true,
currency: true,
length: true,
lockTimeZoneToggleOnBookingPage: true,
requiresConfirmation: true,
requiresBookerEmailVerification: true,
price: true,

View File

@ -79,6 +79,7 @@ export default function CreateEventTypeDialog({
membershipRole: MembershipRole | null | undefined;
}[];
}) {
const utils = trpc.useContext();
const { t } = useLocale();
const router = useRouter();
const [firstRender, setFirstRender] = useState(true);
@ -116,6 +117,7 @@ export default function CreateEventTypeDialog({
const createMutation = trpc.viewer.eventTypes.create.useMutation({
onSuccess: async ({ eventType }) => {
await utils.viewer.eventTypes.getByViewer.invalidate();
await router.replace(`/event-types/${eventType.id}`);
showToast(
t("event_type_created_successfully", {

View File

@ -33,6 +33,7 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
customInputs: true,
disableGuests: true,
metadata: true,
lockTimeZoneToggleOnBookingPage: true,
requiresConfirmation: true,
requiresBookerEmailVerification: true,
recurringEvent: true,

View File

@ -22,6 +22,7 @@ export type WebhookDataType = CalendarEvent &
bookingId?: number;
status?: string;
smsReminderNumber?: string;
rescheduleId?: number;
rescheduleUid?: string;
rescheduleStartTime?: string;
rescheduleEndTime?: string;

View File

@ -86,6 +86,7 @@ const commons = {
recurringEvent: null,
destinationCalendar: null,
team: null,
lockTimeZoneToggleOnBookingPage: false,
requiresConfirmation: false,
requiresBookerEmailVerification: false,
bookingLimits: null,

View File

@ -89,6 +89,7 @@ export default async function getEventTypeById({
periodStartDate: true,
periodEndDate: true,
periodCountCalendarDays: true,
lockTimeZoneToggleOnBookingPage: true,
requiresConfirmation: true,
requiresBookerEmailVerification: true,
recurringEvent: true,

View File

@ -85,6 +85,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
periodDays: null,
periodCountCalendarDays: null,
recurringEvent: null,
lockTimeZoneToggleOnBookingPage: false,
requiresConfirmation: false,
disableGuests: false,
hideCalendarNotes: false,

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EventType" ADD COLUMN "lockTimeZoneToggleOnBookingPage" BOOLEAN NOT NULL DEFAULT false;

View File

@ -86,6 +86,7 @@ model EventType {
periodEndDate DateTime?
periodDays Int?
periodCountCalendarDays Boolean?
lockTimeZoneToggleOnBookingPage Boolean @default(false)
requiresConfirmation Boolean @default(false)
requiresBookerEmailVerification Boolean @default(false)
/// @zod.custom(imports.recurringEventType)

View File

@ -11,6 +11,7 @@ export const baseEventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
hidden: true,
price: true,
currency: true,
lockTimeZoneToggleOnBookingPage: true,
requiresConfirmation: true,
requiresBookerEmailVerification: true,
});
@ -28,6 +29,7 @@ export const bookEventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
periodStartDate: true,
periodEndDate: true,
recurringEvent: true,
lockTimeZoneToggleOnBookingPage: true,
requiresConfirmation: true,
requiresBookerEmailVerification: true,
metadata: true,

View File

@ -3550,7 +3550,7 @@ __metadata:
postcss: ^8.4.18
react: ^18.2.0
react-dom: ^18.2.0
tailwindcss: ^3.3.1
tailwindcss: ^3.3.3
typescript: ^4.9.4
languageName: unknown
linkType: soft
@ -3644,7 +3644,7 @@ __metadata:
"@calcom/ui": "*"
"@headlessui/react": ^1.5.0
"@heroicons/react": ^1.0.6
"@prisma/client": ^5.3.0
"@prisma/client": ^5.4.2
"@tailwindcss/forms": ^0.5.2
"@types/node": 16.9.1
"@types/react": 18.0.26
@ -8245,7 +8245,7 @@ __metadata:
languageName: node
linkType: hard
"@prisma/client@npm:^5.3.0, @prisma/client@npm:^5.4.2":
"@prisma/client@npm:^5.4.2":
version: 5.4.2
resolution: "@prisma/client@npm:5.4.2"
dependencies:
@ -26866,13 +26866,20 @@ __metadata:
languageName: node
linkType: hard
"libphonenumber-js@npm:^1.10.12":
"libphonenumber-js@npm:1.10.12":
version: 1.10.12
resolution: "libphonenumber-js@npm:1.10.12"
checksum: 8b8789b8b46f59e540108cdd3b925990e722a52134e03ab78a360312b7a001ab7f8211320338ddd60b8c68dd49d56075e16567f68aa22698e2218b69300fad42
languageName: node
linkType: hard
"libphonenumber-js@patch:libphonenumber-js@npm%3A1.10.12#./.yarn/patches/libphonenumber-js-npm-1.10.12-51c84f8bf1.patch::locator=calcom-monorepo%40workspace%3A.":
version: 1.10.12
resolution: "libphonenumber-js@patch:libphonenumber-js@npm%3A1.10.12#./.yarn/patches/libphonenumber-js-npm-1.10.12-51c84f8bf1.patch::version=1.10.12&hash=1f76a8&locator=calcom-monorepo%40workspace%3A."
checksum: 9c0940ae91f19a6b69c42f685af0584b273bf7f96984ee9d7a26ec945b0473fb03546638a442cbb3ee1c76ad78801b57bec9d737b6b59ab681a909bec1edfbba
languageName: node
linkType: hard
"libqp@npm:2.0.1":
version: 2.0.1
resolution: "libqp@npm:2.0.1"
@ -29412,7 +29419,7 @@ __metadata:
languageName: node
linkType: hard
"next-i18next@npm:^13.2.2":
"next-i18next@npm:13.3.0":
version: 13.3.0
resolution: "next-i18next@npm:13.3.0"
dependencies:
@ -29430,6 +29437,24 @@ __metadata:
languageName: node
linkType: hard
"next-i18next@patch:next-i18next@npm%3A13.3.0#./.yarn/patches/next-i18next-npm-13.3.0-bf25b0943c.patch::locator=calcom-monorepo%40workspace%3A.":
version: 13.3.0
resolution: "next-i18next@patch:next-i18next@npm%3A13.3.0#./.yarn/patches/next-i18next-npm-13.3.0-bf25b0943c.patch::version=13.3.0&hash=bcbde7&locator=calcom-monorepo%40workspace%3A."
dependencies:
"@babel/runtime": ^7.20.13
"@types/hoist-non-react-statics": ^3.3.1
core-js: ^3
hoist-non-react-statics: ^3.3.2
i18next-fs-backend: ^2.1.1
peerDependencies:
i18next: ^22.0.6
next: ">= 12.0.0"
react: ">= 17.0.2"
react-i18next: ^12.2.0
checksum: 7dcb7e2ec14a0164e2c803b5eb4be3d3198ff0db266fecd6225dfa99ec53bf923fe50230c413f2e9b9a795266fb4e31f129572865181df1eadcf8721ad138b3e
languageName: node
linkType: hard
"next-seo@npm:^6.0.0":
version: 6.1.0
resolution: "next-seo@npm:6.1.0"