Merge branch 'main' into connect-component
This commit is contained in:
commit
bd5256bbaf
25
.env.example
25
.env.example
|
@ -107,6 +107,19 @@ NEXT_PUBLIC_HELPSCOUT_KEY=
|
|||
NEXT_PUBLIC_FRESHCHAT_TOKEN=
|
||||
NEXT_PUBLIC_FRESHCHAT_HOST=
|
||||
|
||||
# Google OAuth credentials
|
||||
# To enable Login with Google you need to:
|
||||
# 1. Set `GOOGLE_API_CREDENTIALS` above
|
||||
# 2. Set `GOOGLE_LOGIN_ENABLED` to `true`
|
||||
# When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance
|
||||
# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications
|
||||
GOOGLE_LOGIN_ENABLED=false
|
||||
|
||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||
# Needed to enable Google Calendar integration and Login with Google
|
||||
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
|
||||
GOOGLE_API_CREDENTIALS=
|
||||
|
||||
# Inbox to send user feedback
|
||||
SEND_FEEDBACK_EMAIL=
|
||||
|
||||
|
@ -237,6 +250,12 @@ AUTH_BEARER_TOKEN_VERCEL=
|
|||
E2E_TEST_APPLE_CALENDAR_EMAIL=""
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD=""
|
||||
|
||||
# - CALCOM QA ACCOUNT
|
||||
# Used for E2E tests on Cal.com that require 3rd party integrations
|
||||
E2E_TEST_CALCOM_QA_EMAIL="qa@example.com"
|
||||
# Replace with your own password
|
||||
E2E_TEST_CALCOM_QA_PASSWORD="password"
|
||||
|
||||
# - APP CREDENTIAL SYNC ***********************************************************************************
|
||||
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
|
||||
# Under settings/admin/apps ensure that all app secrets are set the same as the parent application
|
||||
|
@ -264,3 +283,9 @@ E2E_TEST_OIDC_USER_EMAIL=
|
|||
E2E_TEST_OIDC_USER_PASSWORD=
|
||||
|
||||
# ***********************************************************************************************************
|
||||
|
||||
# provide a value between 0 and 100 to ensure the percentage of traffic
|
||||
# redirected from the legacy to the future pages
|
||||
AB_TEST_BUCKET_PROBABILITY=50
|
||||
# whether we redirect to the future/event-types from event-types or not
|
||||
APP_ROUTER_EVENT_TYPES_ENABLED=1
|
||||
|
|
|
@ -13,4 +13,4 @@ jobs:
|
|||
with:
|
||||
repo-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
organization-name: calcom
|
||||
ignore-labels: "app-store, ai, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
||||
ignore-labels: "app-store, ai, authentication, automated-testing, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
|
||||
|
|
|
@ -316,8 +316,13 @@ async function checkPermissions(req: NextApiRequest) {
|
|||
statusCode: 401,
|
||||
message: "ADMIN required for `userId`",
|
||||
});
|
||||
if (!isAdmin && body.teamId)
|
||||
throw new HttpError({
|
||||
statusCode: 401,
|
||||
message: "ADMIN required for `teamId`",
|
||||
});
|
||||
/* Admin users are required to pass in a userId or teamId */
|
||||
if (isAdmin && (!body.userId || !body.teamId))
|
||||
if (isAdmin && !body.userId && !body.teamId)
|
||||
throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" });
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"@calcom/config": "*",
|
||||
"@calcom/dayjs": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@radix-ui/react-avatar": "^1.0.0",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
|
@ -49,7 +49,7 @@
|
|||
"storybook-addon-designs": "^6.3.1",
|
||||
"storybook-addon-next": "^1.6.9",
|
||||
"storybook-react-i18next": "^1.1.2",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.1.2"
|
||||
}
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
import { getBucket } from "abTest/utils";
|
||||
import type { NextMiddleware, NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { NextResponse, URLPattern } from "next/server";
|
||||
import z from "zod";
|
||||
|
||||
const ROUTES: [RegExp, boolean][] = [
|
||||
[/^\/event-types$/, Boolean(process.env.APP_ROUTER_EVENT_TYPES_ENABLED)],
|
||||
];
|
||||
const ROUTES: [URLPattern, boolean][] = [
|
||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
||||
].map(([pathname, enabled]) => [
|
||||
new URLPattern({
|
||||
pathname,
|
||||
}),
|
||||
enabled,
|
||||
]);
|
||||
|
||||
const FUTURE_ROUTES_OVERRIDE_COOKIE_NAME = "x-calcom-future-routes-override";
|
||||
const FUTURE_ROUTES_ENABLED_COOKIE_NAME = "x-calcom-future-routes-enabled";
|
||||
|
||||
const bucketSchema = z.union([z.literal("legacy"), z.literal("future")]).default("legacy");
|
||||
const bucketSchema = z.union([z.literal("legacy"), z.literal("future")]);
|
||||
|
||||
export const abTestMiddlewareFactory =
|
||||
(next: (req: NextRequest) => Promise<NextResponse<unknown>>): NextMiddleware =>
|
||||
|
@ -21,7 +26,7 @@ export const abTestMiddlewareFactory =
|
|||
|
||||
const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME);
|
||||
|
||||
const route = ROUTES.find(([regExp]) => regExp.test(pathname)) ?? null;
|
||||
const route = ROUTES.find(([regExp]) => regExp.test(req.url)) ?? null;
|
||||
|
||||
const enabled = route !== null ? route[1] || override : false;
|
||||
|
||||
|
@ -35,16 +40,29 @@ export const abTestMiddlewareFactory =
|
|||
|
||||
if (!safeParsedBucket.success) {
|
||||
// cookie does not exist or it has incorrect value
|
||||
const bucket = getBucket();
|
||||
|
||||
const res = NextResponse.next(response);
|
||||
res.cookies.set(FUTURE_ROUTES_ENABLED_COOKIE_NAME, getBucket(), { expires: 1000 * 60 * 30 }); // 30 min in ms
|
||||
return res;
|
||||
response.cookies.set(FUTURE_ROUTES_ENABLED_COOKIE_NAME, bucket, {
|
||||
expires: Date.now() + 1000 * 60 * 30,
|
||||
httpOnly: true,
|
||||
}); // 30 min in ms
|
||||
|
||||
if (bucket === "legacy") {
|
||||
return response;
|
||||
}
|
||||
|
||||
const url = req.nextUrl.clone();
|
||||
url.pathname = `future${pathname}/`;
|
||||
|
||||
return NextResponse.rewrite(url, response);
|
||||
}
|
||||
|
||||
const bucketUrlPrefix = safeParsedBucket.data === "future" ? "future" : "";
|
||||
if (safeParsedBucket.data === "legacy") {
|
||||
return response;
|
||||
}
|
||||
|
||||
const url = req.nextUrl.clone();
|
||||
url.pathname = `${bucketUrlPrefix}${pathname}/`;
|
||||
url.pathname = `future${pathname}/`;
|
||||
|
||||
return NextResponse.rewrite(url, response);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { createHydrateClient } from "app/_trpc/createHydrateClient";
|
||||
import superjson from "superjson";
|
||||
|
||||
export const HydrateClient = createHydrateClient({
|
||||
transformer: superjson,
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
import { createTRPCReact } from "@trpc/react-query";
|
||||
|
||||
export const trpc = createTRPCReact<AppRouter>({});
|
|
@ -0,0 +1,21 @@
|
|||
"use client";
|
||||
|
||||
import { type DehydratedState, Hydrate } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import type { DataTransformer } from "@trpc/server";
|
||||
|
||||
export function createHydrateClient(opts: { transformer?: DataTransformer }) {
|
||||
return function HydrateClient(props: { children: React.ReactNode; state: DehydratedState }) {
|
||||
const { state, children } = props;
|
||||
|
||||
const transformedState: DehydratedState = useMemo(() => {
|
||||
if (opts.transformer) {
|
||||
return opts.transformer.deserialize(state);
|
||||
}
|
||||
return state;
|
||||
}, [state]);
|
||||
|
||||
return <Hydrate state={transformedState}>{children}</Hydrate>;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
import { type DehydratedState, QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { HydrateClient } from "app/_trpc/HydrateClient";
|
||||
import { trpc } from "app/_trpc/client";
|
||||
import { useState } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
|
||||
import { httpLink } from "@calcom/trpc/client/links/httpLink";
|
||||
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
|
||||
import { splitLink } from "@calcom/trpc/client/links/splitLink";
|
||||
|
||||
const ENDPOINTS = [
|
||||
"admin",
|
||||
"apiKeys",
|
||||
"appRoutingForms",
|
||||
"apps",
|
||||
"auth",
|
||||
"availability",
|
||||
"appBasecamp3",
|
||||
"bookings",
|
||||
"deploymentSetup",
|
||||
"eventTypes",
|
||||
"features",
|
||||
"insights",
|
||||
"payments",
|
||||
"public",
|
||||
"saml",
|
||||
"slots",
|
||||
"teams",
|
||||
"organizations",
|
||||
"users",
|
||||
"viewer",
|
||||
"webhook",
|
||||
"workflows",
|
||||
"appsRouter",
|
||||
"googleWorkspace",
|
||||
] as const;
|
||||
export type Endpoint = (typeof ENDPOINTS)[number];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const resolveEndpoint = (links: any) => {
|
||||
// TODO: Update our trpc routes so they are more clear.
|
||||
// This function parses paths like the following and maps them
|
||||
// to the correct API endpoints.
|
||||
// - viewer.me - 2 segment paths like this are for logged in requests
|
||||
// - viewer.public.i18n - 3 segments paths can be public or authed
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (ctx: any) => {
|
||||
const parts = ctx.op.path.split(".");
|
||||
let endpoint;
|
||||
let path = "";
|
||||
if (parts.length == 2) {
|
||||
endpoint = parts[0] as keyof typeof links;
|
||||
path = parts[1];
|
||||
} else {
|
||||
endpoint = parts[1] as keyof typeof links;
|
||||
path = parts.splice(2, parts.length - 2).join(".");
|
||||
}
|
||||
return links[endpoint]({ ...ctx, op: { ...ctx.op, path } });
|
||||
};
|
||||
};
|
||||
|
||||
export const TrpcProvider: React.FC<{ children: React.ReactNode; dehydratedState?: DehydratedState }> = ({
|
||||
children,
|
||||
dehydratedState,
|
||||
}) => {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: { queries: { staleTime: 5000 } },
|
||||
})
|
||||
);
|
||||
const url =
|
||||
typeof window !== "undefined"
|
||||
? "/api/trpc"
|
||||
: process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}/api/trpc`
|
||||
: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
|
||||
|
||||
const [trpcClient] = useState(() =>
|
||||
trpc.createClient({
|
||||
links: [
|
||||
// adds pretty logs to your console in development and logs errors in production
|
||||
loggerLink({
|
||||
enabled: (opts) =>
|
||||
!!process.env.NEXT_PUBLIC_DEBUG || (opts.direction === "down" && opts.result instanceof Error),
|
||||
}),
|
||||
splitLink({
|
||||
// check for context property `skipBatch`
|
||||
condition: (op) => !!op.context.skipBatch,
|
||||
// when condition is true, use normal request
|
||||
true: (runtime) => {
|
||||
const links = Object.fromEntries(
|
||||
ENDPOINTS.map((endpoint) => [
|
||||
endpoint,
|
||||
httpLink({
|
||||
url: `${url}/${endpoint}`,
|
||||
})(runtime),
|
||||
])
|
||||
);
|
||||
return resolveEndpoint(links);
|
||||
},
|
||||
// when condition is false, use batch request
|
||||
false: (runtime) => {
|
||||
const links = Object.fromEntries(
|
||||
ENDPOINTS.map((endpoint) => [
|
||||
endpoint,
|
||||
httpBatchLink({
|
||||
url: `${url}/${endpoint}`,
|
||||
})(runtime),
|
||||
])
|
||||
);
|
||||
return resolveEndpoint(links);
|
||||
},
|
||||
}),
|
||||
],
|
||||
transformer: superjson,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{dehydratedState ? <HydrateClient state={dehydratedState}>{children}</HydrateClient> : children}
|
||||
</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
import { type TFunction } from "i18next";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { constructGenericImage } from "@calcom/lib/OgImages";
|
||||
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG } from "@calcom/lib/constants";
|
||||
import { getFixedT } from "@calcom/lib/server/getFixedT";
|
||||
|
||||
import { preparePageMetadata } from "@lib/metadata";
|
||||
|
||||
export const _generateMetadata = async (
|
||||
getTitle: (t: TFunction<string, undefined>) => string,
|
||||
getDescription: (t: TFunction<string, undefined>) => string
|
||||
) => {
|
||||
const h = headers();
|
||||
const canonical = h.get("x-pathname") ?? "";
|
||||
const locale = h.get("x-locale") ?? "en";
|
||||
|
||||
const t = await getFixedT(locale, "common");
|
||||
|
||||
const title = getTitle(t);
|
||||
const description = getDescription(t);
|
||||
|
||||
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
|
||||
|
||||
const image =
|
||||
SEO_IMG_OGIMG +
|
||||
constructGenericImage({
|
||||
title,
|
||||
description,
|
||||
});
|
||||
|
||||
return preparePageMetadata({
|
||||
title,
|
||||
canonical,
|
||||
image,
|
||||
description,
|
||||
siteName: APP_NAME,
|
||||
metadataBase,
|
||||
});
|
||||
};
|
|
@ -0,0 +1,64 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* Typescript class based component for custom-error
|
||||
* @link https://nextjs.org/docs/advanced-features/custom-error-page
|
||||
*/
|
||||
import type { NextPage } from "next";
|
||||
import type { ErrorProps } from "next/error";
|
||||
import React from "react";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { redactError } from "@calcom/lib/redactError";
|
||||
|
||||
import { ErrorPage } from "@components/error/error-page";
|
||||
|
||||
type NextError = Error & { digest?: string };
|
||||
|
||||
// Ref: https://nextjs.org/docs/app/api-reference/file-conventions/error#props
|
||||
export type DefaultErrorProps = {
|
||||
error: NextError;
|
||||
reset: () => void; // A function to reset the error boundary
|
||||
};
|
||||
|
||||
type AugmentedError = NextError | HttpError | null;
|
||||
|
||||
type CustomErrorProps = {
|
||||
err?: AugmentedError;
|
||||
statusCode?: number;
|
||||
message?: string;
|
||||
} & Omit<ErrorProps, "err" | "statusCode">;
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[error]"] });
|
||||
|
||||
const CustomError: NextPage<DefaultErrorProps> = (props) => {
|
||||
const { error } = props;
|
||||
let errorObject: CustomErrorProps = {
|
||||
message: error.message,
|
||||
err: error,
|
||||
};
|
||||
|
||||
if (error instanceof HttpError) {
|
||||
const redactedError = redactError(error);
|
||||
errorObject = {
|
||||
statusCode: error.statusCode,
|
||||
title: redactedError.name,
|
||||
message: redactedError.message,
|
||||
err: {
|
||||
...redactedError,
|
||||
...error,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// `error.digest` property contains an automatically generated hash of the error that can be used to match the corresponding error in server-side logs
|
||||
log.debug(`${error?.toString() ?? JSON.stringify(error)}`);
|
||||
log.info("errorObject: ", errorObject);
|
||||
|
||||
return (
|
||||
<ErrorPage statusCode={errorObject.statusCode} error={errorObject.err} message={errorObject.message} />
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomError;
|
|
@ -0,0 +1,10 @@
|
|||
import EventTypes from "@pages/event-types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("event_types_page_title"),
|
||||
(t) => t("event_types_page_subtitle")
|
||||
);
|
||||
|
||||
export default EventTypes;
|
|
@ -0,0 +1,22 @@
|
|||
// pages without layout (e.g., /availability/index.tsx) are supposed to go under (layout) folder
|
||||
import { headers } from "next/headers";
|
||||
import { type ReactElement } from "react";
|
||||
|
||||
import { getLayout } from "@calcom/features/MainLayoutAppDir";
|
||||
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
type WrapperWithLayoutProps = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) {
|
||||
const h = headers();
|
||||
const nonce = h.get("x-nonce") ?? undefined;
|
||||
|
||||
return (
|
||||
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
|
||||
{children}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
"use client";
|
||||
|
||||
import { type NextPage } from "next";
|
||||
|
||||
import CustomError, { type DefaultErrorProps } from "./error";
|
||||
|
||||
export const GlobalError: NextPage<DefaultErrorProps> = (props) => {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<CustomError {...props} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalError;
|
|
@ -1,84 +1,63 @@
|
|||
import type { Metadata } from "next";
|
||||
import { headers as nextHeaders, cookies as nextCookies } from "next/headers";
|
||||
import { dir } from "i18next";
|
||||
import { headers, cookies } 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 { prepareRootMetadata } from "@lib/metadata";
|
||||
|
||||
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",
|
||||
export const generateMetadata = () =>
|
||||
prepareRootMetadata({
|
||||
twitterCreator: "@calcom",
|
||||
twitterSite: "@calcom",
|
||||
robots: {
|
||||
index: false,
|
||||
follow: false,
|
||||
},
|
||||
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 getInitialProps = async (url: string) => {
|
||||
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);
|
||||
}
|
||||
const req = { headers: headers(), cookies: cookies() };
|
||||
const newLocale = await getLocale(req);
|
||||
const direction = dir(newLocale);
|
||||
|
||||
return { isEmbed, embedColorScheme, locale: newLocale, direction };
|
||||
};
|
||||
|
||||
const getFallbackProps = () => ({
|
||||
locale: "en",
|
||||
direction: "ltr",
|
||||
isEmbed: false,
|
||||
embedColorScheme: false,
|
||||
});
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const headers = nextHeaders();
|
||||
const cookies = nextCookies();
|
||||
const h = headers();
|
||||
|
||||
const fullUrl = headers.get("x-url") ?? "";
|
||||
const nonce = headers.get("x-csp") ?? "";
|
||||
const fullUrl = h.get("x-url") ?? "";
|
||||
const nonce = h.get("x-csp") ?? "";
|
||||
|
||||
const isSSG = !fullUrl;
|
||||
|
||||
const { locale, direction, isEmbed, embedColorScheme } = isSSG
|
||||
? getFallbackProps()
|
||||
: await getInitialProps(fullUrl);
|
||||
|
||||
const { locale, direction, isEmbed, embedColorScheme } = await getInitialProps(fullUrl, headers, cookies);
|
||||
return (
|
||||
<html
|
||||
lang={locale}
|
||||
dir={direction}
|
||||
style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}>
|
||||
style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}
|
||||
data-nextjs-router="app">
|
||||
<head nonce={nonce}>
|
||||
{!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && (
|
||||
// eslint-disable-next-line @next/next/no-sync-scripts
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { type DehydratedState } from "@tanstack/react-query";
|
||||
import type { SSRConfig } from "next-i18next";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
|
@ -10,7 +11,6 @@ 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";
|
||||
|
@ -29,13 +29,14 @@ const calFont = localFont({
|
|||
});
|
||||
|
||||
export type PageWrapperProps = Readonly<{
|
||||
getLayout: (page: React.ReactElement) => ReactNode;
|
||||
getLayout: ((page: React.ReactElement) => ReactNode) | null;
|
||||
children: React.ReactElement;
|
||||
requiresLicense: boolean;
|
||||
isThemeSupported: boolean;
|
||||
isBookingPage: boolean;
|
||||
nonce: string | undefined;
|
||||
themeBasis: string | null;
|
||||
dehydratedState?: DehydratedState;
|
||||
isThemeSupported?: boolean;
|
||||
isBookingPage?: boolean;
|
||||
i18n?: SSRConfig;
|
||||
}>;
|
||||
|
||||
|
@ -85,4 +86,4 @@ function PageWrapper(props: PageWrapperProps) {
|
|||
);
|
||||
}
|
||||
|
||||
export default trpc.withTRPC(PageWrapper);
|
||||
export default PageWrapper;
|
||||
|
|
|
@ -76,6 +76,8 @@ const ChildrenEventTypesList = ({
|
|||
<div>
|
||||
<Label>{t("assign_to")}</Label>
|
||||
<ChildrenEventTypeSelect
|
||||
aria-label="assignment-dropdown"
|
||||
data-testid="assignment-dropdown"
|
||||
onChange={(options) => {
|
||||
onChange &&
|
||||
onChange(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { TrpcProvider } from "app/_trpc/trpc-provider";
|
||||
import { dir } from "i18next";
|
||||
import type { Session } from "next-auth";
|
||||
import { SessionProvider, useSession } from "next-auth/react";
|
||||
|
@ -255,26 +256,28 @@ const AppProviders = (props: PageWrapperProps) => {
|
|||
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>
|
||||
<TrpcProvider dehydratedState={props.dehydratedState}>
|
||||
<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={/* undefined gets treated as true */ props.isThemeSupported ?? true}
|
||||
isBookingPage={props.isBookingPage || isBookingPage}>
|
||||
<FeatureFlagsProvider>
|
||||
<OrgBrandProvider>
|
||||
<MetaProvider>{props.children}</MetaProvider>
|
||||
</OrgBrandProvider>
|
||||
</FeatureFlagsProvider>
|
||||
</CalcomThemeProvider>
|
||||
</TooltipProvider>
|
||||
</CustomI18nextProvider>
|
||||
</SessionProvider>
|
||||
</EventCollectionProvider>
|
||||
</TrpcProvider>
|
||||
);
|
||||
|
||||
if (isBookingPage) {
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import type { Metadata } from "next";
|
||||
|
||||
import { truncateOnWord } from "@calcom/lib/text";
|
||||
|
||||
type RootMetadataRecipe = Readonly<{
|
||||
twitterCreator: string;
|
||||
twitterSite: string;
|
||||
robots: {
|
||||
index: boolean;
|
||||
follow: boolean;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type PageMetadataRecipe = Readonly<{
|
||||
title: string;
|
||||
canonical: string;
|
||||
image: string;
|
||||
description: string;
|
||||
siteName: string;
|
||||
metadataBase: URL;
|
||||
}>;
|
||||
|
||||
export const prepareRootMetadata = (recipe: RootMetadataRecipe): Metadata => ({
|
||||
icons: {
|
||||
icon: "/favicon.icon",
|
||||
apple: "/api/logo?type=apple-touch-icon",
|
||||
other: [
|
||||
{
|
||||
rel: "icon-mask",
|
||||
url: "/safari-pinned-tab.svg",
|
||||
color: "#000000",
|
||||
},
|
||||
{
|
||||
url: "/api/logo?type=favicon-16",
|
||||
sizes: "16x16",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
url: "/api/logo?type=favicon-32",
|
||||
sizes: "32x32",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
manifest: "/site.webmanifest",
|
||||
viewport: "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0",
|
||||
robots: recipe.robots,
|
||||
other: {
|
||||
"application-TileColor": "#ff0000",
|
||||
},
|
||||
themeColor: [
|
||||
{
|
||||
media: "(prefers-color-scheme: light)",
|
||||
color: "#f9fafb",
|
||||
},
|
||||
{
|
||||
media: "(prefers-color-scheme: dark)",
|
||||
color: "#1C1C1C",
|
||||
},
|
||||
],
|
||||
twitter: {
|
||||
site: recipe.twitterSite,
|
||||
creator: recipe.twitterCreator,
|
||||
card: "summary_large_image",
|
||||
},
|
||||
});
|
||||
|
||||
export const preparePageMetadata = (recipe: PageMetadataRecipe): Metadata => ({
|
||||
title: recipe.title,
|
||||
alternates: {
|
||||
canonical: recipe.canonical,
|
||||
},
|
||||
openGraph: {
|
||||
description: truncateOnWord(recipe.description, 158),
|
||||
url: recipe.canonical,
|
||||
type: "website",
|
||||
siteName: recipe.siteName,
|
||||
title: recipe.title,
|
||||
images: [recipe.image],
|
||||
},
|
||||
metadataBase: recipe.metadataBase,
|
||||
});
|
|
@ -1,13 +1,16 @@
|
|||
import { get } from "@vercel/edge-config";
|
||||
import { collectEvents } from "next-collect/server";
|
||||
import type { NextMiddleware } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry";
|
||||
|
||||
import { csp } from "@lib/csp";
|
||||
|
||||
const middleware: NextMiddleware = async (req) => {
|
||||
import { abTestMiddlewareFactory } from "./abTest/middlewareFactory";
|
||||
|
||||
const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
|
||||
const url = req.nextUrl;
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
|
||||
|
@ -61,6 +64,12 @@ const middleware: NextMiddleware = async (req) => {
|
|||
requestHeaders.set("x-csp-enforce", "true");
|
||||
}
|
||||
|
||||
requestHeaders.set("x-pathname", url.pathname);
|
||||
|
||||
const locale = await getLocale(req);
|
||||
|
||||
requestHeaders.set("x-locale", locale);
|
||||
|
||||
return NextResponse.next({
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
|
@ -90,11 +99,13 @@ export const config = {
|
|||
* Paths required by routingForms.handle
|
||||
*/
|
||||
"/apps/routing_forms/:path*",
|
||||
"/event-types",
|
||||
"/future/event-types/",
|
||||
],
|
||||
};
|
||||
|
||||
export default collectEvents({
|
||||
middleware,
|
||||
middleware: abTestMiddlewareFactory(middleware),
|
||||
...nextCollectBasicSettings,
|
||||
cookieName: "__clnds",
|
||||
extend: extendEventData,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.5.0",
|
||||
"version": "3.5.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
@ -45,7 +45,7 @@
|
|||
"@hookform/resolvers": "^2.9.7",
|
||||
"@next-auth/prisma-adapter": "^1.0.4",
|
||||
"@next/bundle-analyzer": "^13.1.6",
|
||||
"@radix-ui/react-avatar": "^1.0.0",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
|
@ -170,7 +170,7 @@
|
|||
"node-html-parser": "^6.1.10",
|
||||
"node-mocks-http": "^1.11.0",
|
||||
"postcss": "^8.4.18",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tailwindcss-animate": "^1.0.6",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { User } from "@prisma/client";
|
||||
import { Trans } from "next-i18next";
|
||||
|
|
|
@ -37,7 +37,7 @@ type SignupProps = inferSSRProps<typeof getServerSideProps>;
|
|||
const checkValidEmail = (email: string) => z.string().email().safeParse(email).success;
|
||||
|
||||
const getOrgUsernameFromEmail = (email: string, autoAcceptEmailDomain: string) => {
|
||||
const [emailUser, emailDomain] = email.split("@");
|
||||
const [emailUser, emailDomain = ""] = email.split("@");
|
||||
const username =
|
||||
emailDomain === autoAcceptEmailDomain
|
||||
? slugify(emailUser)
|
||||
|
@ -143,7 +143,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug, orgAutoA
|
|||
methods.clearErrors("apiError");
|
||||
}
|
||||
|
||||
if (methods.getValues().username === undefined && isOrgInviteByLink && orgAutoAcceptEmail) {
|
||||
if (!methods.getValues().username && isOrgInviteByLink && orgAutoAcceptEmail) {
|
||||
methods.setValue(
|
||||
"username",
|
||||
getOrgUsernameFromEmail(methods.getValues().email, orgAutoAcceptEmail)
|
||||
|
|
|
@ -5,16 +5,47 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
|||
import { randomString } from "@calcom/lib/random";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
|
||||
import { bookTimeSlot, createNewEventType, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Event Types A/B tests", () => {
|
||||
test("should point to the /future/event-types page", async ({ page, users, context }) => {
|
||||
await context.addCookies([
|
||||
{
|
||||
name: "x-calcom-future-routes-override",
|
||||
value: "1",
|
||||
url: "http://localhost:3000",
|
||||
},
|
||||
]);
|
||||
const user = await users.create();
|
||||
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.waitForLoadState();
|
||||
|
||||
const dataNextJsRouter = await page.evaluate(() =>
|
||||
window.document.documentElement.getAttribute("data-nextjs-router")
|
||||
);
|
||||
|
||||
expect(dataNextJsRouter).toEqual("app");
|
||||
|
||||
const locator = page.getByRole("heading", { name: "Event Types" });
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Event Types tests", () => {
|
||||
test.describe("user", () => {
|
||||
testBothFutureAndLegacyRoutes.describe("user", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
|
||||
// We wait until loading is finished
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
});
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
E2E_CLIPBOARD_VALUE?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export type Window = typeof window;
|
||||
// creates the single server fixture
|
||||
export const createClipboardFixture = (page: Page) => {
|
||||
return {
|
||||
reset: async () => {
|
||||
await page.evaluate(() => {
|
||||
delete window.E2E_CLIPBOARD_VALUE;
|
||||
});
|
||||
},
|
||||
get: async () => {
|
||||
return getClipboardValue({ page });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function getClipboardValue({ page }: { page: Page }) {
|
||||
return page.evaluate(() => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
setInterval(() => {
|
||||
if (!window.E2E_CLIPBOARD_VALUE) return;
|
||||
resolve(window.E2E_CLIPBOARD_VALUE);
|
||||
}, 500);
|
||||
setTimeout(() => reject(new Error("Timeout")), 1000);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -204,7 +204,7 @@ export function createBookingPageFixture(page: Page) {
|
|||
placeholder?: string
|
||||
) => {
|
||||
await page.getByTestId("add-field").click();
|
||||
await page.locator("#test-field-type > .bg-default > div > div:nth-child(2)").first().click();
|
||||
await page.getByTestId("test-field-type").click();
|
||||
await page.getByTestId(`select-option-${questionType}`).click();
|
||||
await page.getByLabel("Identifier").dblclick();
|
||||
await page.getByLabel("Identifier").fill(identifier);
|
||||
|
|
|
@ -86,12 +86,14 @@ const createTeamAndAddUser = async (
|
|||
user,
|
||||
isUnpublished,
|
||||
isOrg,
|
||||
isOrgVerified,
|
||||
hasSubteam,
|
||||
organizationId,
|
||||
}: {
|
||||
user: { id: number; username: string | null; role?: MembershipRole };
|
||||
user: { id: number; email: string; username: string | null; role?: MembershipRole };
|
||||
isUnpublished?: boolean;
|
||||
isOrg?: boolean;
|
||||
isOrgVerified?: boolean;
|
||||
hasSubteam?: true;
|
||||
organizationId?: number | null;
|
||||
},
|
||||
|
@ -103,7 +105,14 @@ const createTeamAndAddUser = async (
|
|||
};
|
||||
data.metadata = {
|
||||
...(isUnpublished ? { requestedSlug: slug } : {}),
|
||||
...(isOrg ? { isOrganization: true } : {}),
|
||||
...(isOrg
|
||||
? {
|
||||
isOrganization: true,
|
||||
isOrganizationVerified: !!isOrgVerified,
|
||||
orgAutoAcceptEmail: user.email.split("@")[1],
|
||||
isOrganizationConfigured: false,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
data.slug = !isUnpublished ? slug : undefined;
|
||||
if (isOrg && hasSubteam) {
|
||||
|
@ -145,6 +154,7 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
|
|||
teamEventSlug?: string;
|
||||
teamEventLength?: number;
|
||||
isOrg?: boolean;
|
||||
isOrgVerified?: boolean;
|
||||
hasSubteam?: true;
|
||||
isUnpublished?: true;
|
||||
} = {}
|
||||
|
@ -292,9 +302,10 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
|
|||
if (scenario.hasTeam) {
|
||||
const team = await createTeamAndAddUser(
|
||||
{
|
||||
user: { id: user.id, username: user.username, role: "OWNER" },
|
||||
user: { id: user.id, email: user.email, username: user.username, role: "OWNER" },
|
||||
isUnpublished: scenario.isUnpublished,
|
||||
isOrg: scenario.isOrg,
|
||||
isOrgVerified: scenario.isOrgVerified,
|
||||
hasSubteam: scenario.hasSubteam,
|
||||
organizationId: opts?.organizationId,
|
||||
},
|
||||
|
@ -385,6 +396,15 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
|
|||
await prisma.user.delete({ where: { id } });
|
||||
store.users = store.users.filter((b) => b.id !== id);
|
||||
},
|
||||
set: async (email: string) => {
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: { email },
|
||||
include: userIncludes,
|
||||
});
|
||||
const userFixture = createUserFixture(user, store.page);
|
||||
store.users.push(userFixture);
|
||||
return userFixture;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -409,7 +429,11 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
|||
eventTypes: user.eventTypes,
|
||||
routingForms: user.routingForms,
|
||||
self,
|
||||
apiLogin: async () => apiLogin({ ...(await self()), password: user.username }, store.page),
|
||||
apiLogin: async (password?: string) =>
|
||||
apiLogin({ ...(await self()), password: password || user.username }, store.page),
|
||||
/**
|
||||
* @deprecated use apiLogin instead
|
||||
*/
|
||||
login: async () => login({ ...(await self()), password: user.username }, store.page),
|
||||
logout: async () => {
|
||||
await page.goto("/auth/logout");
|
||||
|
|
|
@ -267,7 +267,7 @@ test.describe("Stripe integration", () => {
|
|||
await page.getByTestId("price-input-stripe").fill("200");
|
||||
|
||||
// Select currency in dropdown
|
||||
await page.locator(".text-black > .bg-default > div > div:nth-child(2)").first().click();
|
||||
await page.getByTestId("stripe-currency-select").click();
|
||||
await page.locator("#react-select-2-input").fill("mexi");
|
||||
await page.locator("#react-select-2-option-81").click();
|
||||
|
||||
|
|
|
@ -4,10 +4,12 @@ import type { API } from "mailhog";
|
|||
import mailhog from "mailhog";
|
||||
|
||||
import { IS_MAILHOG_ENABLED } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { ExpectedUrlDetails } from "../../../../playwright.config";
|
||||
import { createBookingsFixture } from "../fixtures/bookings";
|
||||
import { createClipboardFixture } from "../fixtures/clipboard";
|
||||
import { createEmbedsFixture } from "../fixtures/embeds";
|
||||
import { createOrgsFixture } from "../fixtures/orgs";
|
||||
import { createPaymentsFixture } from "../fixtures/payments";
|
||||
|
@ -28,6 +30,7 @@ export interface Fixtures {
|
|||
emails?: API;
|
||||
routingForms: ReturnType<typeof createRoutingFormsFixture>;
|
||||
bookingPage: ReturnType<typeof createBookingPageFixture>;
|
||||
clipboard: ReturnType<typeof createClipboardFixture>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -85,6 +88,8 @@ export const test = base.extend<Fixtures>({
|
|||
const mailhogAPI = mailhog();
|
||||
await use(mailhogAPI);
|
||||
} else {
|
||||
//FIXME: Ideally we should error out here. If someone is running tests with mailhog disabled, they should be aware of it
|
||||
logger.warn("Mailhog is not enabled - Skipping Emails verification");
|
||||
await use(undefined);
|
||||
}
|
||||
},
|
||||
|
@ -92,4 +97,8 @@ export const test = base.extend<Fixtures>({
|
|||
const bookingPage = createBookingPageFixture(page);
|
||||
await use(bookingPage);
|
||||
},
|
||||
clipboard: async ({ page }, use) => {
|
||||
const clipboard = createClipboardFixture(page);
|
||||
await use(clipboard);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { test } from "./fixtures";
|
||||
|
||||
export type RouteVariant = "future" | "legacy";
|
||||
|
||||
const routeVariants = ["future", "legacy"];
|
||||
|
||||
/**
|
||||
* Small wrapper around test.describe().
|
||||
* When using testbothFutureLegacyRoutes.describe() instead of test.describe(), this will run the specified
|
||||
* tests twice. One with the pages route, and one with the new app dir "future" route. It will also add the route variant
|
||||
* name to the test name for easier debugging.
|
||||
* Finally it also adds a parameter routeVariant to your testBothFutureAndLegacyRoutes.describe() callback, which
|
||||
* can be used to do any conditional rendering in the test for a specific route variant (should be as little
|
||||
* as possible).
|
||||
*
|
||||
* See apps/web/playwright/event-types.e2e.ts for an example.
|
||||
*/
|
||||
export const testBothFutureAndLegacyRoutes = {
|
||||
describe: (testName: string, testFn: (routeVariant: RouteVariant) => void) => {
|
||||
routeVariants.forEach((routeVariant) => {
|
||||
test.describe(`${testName} -- ${routeVariant}`, () => {
|
||||
if (routeVariant === "future") {
|
||||
test.beforeEach(({ context }) => {
|
||||
context.addCookies([
|
||||
{ name: "x-calcom-future-routes-override", value: "1", url: "http://localhost:3000" },
|
||||
]);
|
||||
});
|
||||
}
|
||||
testFn(routeVariant as RouteVariant);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
|
@ -1,11 +1,13 @@
|
|||
import type { Frame, Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { createHash } from "crypto";
|
||||
import EventEmitter from "events";
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import { createServer } from "http";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { noop } from "lodash";
|
||||
import type { API, Messages } from "mailhog";
|
||||
import { totp } from "otplib";
|
||||
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
@ -278,3 +280,12 @@ export async function createUserWithSeatedEventAndAttendees(
|
|||
});
|
||||
return { user, eventType, booking };
|
||||
}
|
||||
|
||||
export function generateTotpCode(email: string) {
|
||||
const secret = createHash("md5")
|
||||
.update(email + process.env.CALENDSO_ENCRYPTION_KEY)
|
||||
.digest("hex");
|
||||
|
||||
totp.options = { step: 90 };
|
||||
return totp.generate(secret);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { expect } from "@playwright/test";
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { selectFirstAvailableTimeSlotNextMonth, bookTimeSlot } from "./lib/testUtils";
|
||||
import { localize } from "./lib/testUtils";
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
|
@ -69,15 +72,34 @@ test.describe("Managed Event Types tests", () => {
|
|||
await page.goto("/event-types");
|
||||
await page.getByTestId("event-types").locator('a[title="managed"]').click();
|
||||
await page.getByTestId("vertical-tab-assignment").click();
|
||||
await page.locator('[class$="control"]').filter({ hasText: "Select..." }).click();
|
||||
await page.getByTestId("assignment-dropdown").click();
|
||||
|
||||
await page.getByTestId(`select-option-${memberUser.id}`).click();
|
||||
await page.locator('[type="submit"]').click();
|
||||
await page.getByTestId("toast-success").waitFor();
|
||||
});
|
||||
|
||||
await adminUser.logout();
|
||||
await test.step("Managed event type can use Organizer's default app as location", async () => {
|
||||
await page.getByTestId("vertical-tab-event_setup_tab_title").click();
|
||||
|
||||
await page.locator("#location-select").click();
|
||||
const optionText = (await localize("en"))("organizer_default_conferencing_app");
|
||||
await page.locator(`text=${optionText}`).click();
|
||||
await page.locator("[data-testid=update-eventtype]").click();
|
||||
await page.getByTestId("toast-success").waitFor();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.getByTestId("vertical-tab-assignment").click();
|
||||
await gotoBookingPage(page);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step("Managed event type has locked fields for added member", async () => {
|
||||
await adminUser.logout();
|
||||
|
||||
// Coming back as member user to see if there is a managed event present after assignment
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
|
@ -91,3 +113,9 @@ test.describe("Managed Event Types tests", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function gotoBookingPage(page: Page) {
|
||||
const previewLink = await page.getByTestId("preview-button").getAttribute("href");
|
||||
|
||||
await page.goto(previewLink ?? "");
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { JSDOM } from "jsdom";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import type { API, Messages } from "mailhog";
|
||||
|
||||
import { getEmailsReceivedByUser } from "../lib/testUtils";
|
||||
|
||||
export async function expectInvitationEmailToBeReceived(
|
||||
page: Page,
|
||||
emails: API | undefined,
|
||||
userEmail: string,
|
||||
subject: string,
|
||||
returnLink?: string
|
||||
) {
|
||||
if (!emails) return null;
|
||||
// We need to wait for the email to go through, otherwise it will fail
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(5000);
|
||||
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail });
|
||||
expect(receivedEmails?.total).toBe(1);
|
||||
const [firstReceivedEmail] = (receivedEmails as Messages).items;
|
||||
expect(firstReceivedEmail.subject).toBe(subject);
|
||||
if (!returnLink) return;
|
||||
const dom = new JSDOM(firstReceivedEmail.html);
|
||||
const anchor = dom.window.document.querySelector(`a[href*="${returnLink}"]`);
|
||||
return anchor?.getAttribute("href");
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
import { expect } from "@playwright/test";
|
||||
import path from "path";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { generateTotpCode } from "../lib/testUtils";
|
||||
import { expectInvitationEmailToBeReceived } from "./expects";
|
||||
|
||||
test.afterAll(({ users, emails }) => {
|
||||
users.deleteAll();
|
||||
emails?.deleteAll();
|
||||
});
|
||||
|
||||
function capitalize(text: string) {
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
}
|
||||
|
||||
test.describe("Organization", () => {
|
||||
test("should be able to create an organization and complete onboarding", async ({
|
||||
page,
|
||||
users,
|
||||
emails,
|
||||
}) => {
|
||||
const orgOwner = await users.create();
|
||||
const orgDomain = `${orgOwner.username}-org`;
|
||||
const orgName = capitalize(`${orgOwner.username}-org`);
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto("/settings/organizations/new");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("Basic info", async () => {
|
||||
// Check required fields
|
||||
await page.locator("button[type=submit]").click();
|
||||
await expect(page.locator(".text-red-700")).toHaveCount(3);
|
||||
|
||||
// Happy path
|
||||
await page.locator("input[name=adminEmail]").fill(`john@${orgDomain}.com`);
|
||||
expect(await page.locator("input[name=name]").inputValue()).toEqual(orgName);
|
||||
expect(await page.locator("input[name=slug]").inputValue()).toEqual(orgDomain);
|
||||
await page.locator("button[type=submit]").click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Check admin email about code verification
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
`john@${orgOwner.username}-org.com`,
|
||||
"Verify your email to create an organization"
|
||||
);
|
||||
|
||||
await test.step("Verification", async () => {
|
||||
// Code verification
|
||||
await expect(page.locator("#modal-title")).toBeVisible();
|
||||
await page.locator("input[name='2fa1']").fill(generateTotpCode(`john@${orgDomain}.com`));
|
||||
await page.locator("button:text('Verify')").click();
|
||||
|
||||
// Check admin email about DNS pending action
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
"admin@example.com",
|
||||
"New organization created: pending action"
|
||||
);
|
||||
|
||||
// Waiting to be in next step URL
|
||||
await page.waitForURL("/settings/organizations/*/set-password");
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Admin password", async () => {
|
||||
// Check required fields
|
||||
await page.locator("button[type=submit]").click();
|
||||
await expect(page.locator(".text-red-700")).toHaveCount(3); // 3 password hints
|
||||
|
||||
// Happy path
|
||||
await page.locator("input[name='password']").fill("ADMIN_user2023$");
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
// Waiting to be in next step URL
|
||||
await page.waitForURL("/settings/organizations/*/about");
|
||||
});
|
||||
|
||||
await test.step("About the organization", async () => {
|
||||
// Choosing an avatar
|
||||
await page.locator('button:text("Upload")').click();
|
||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||
await page.getByText("Choose a file...").click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(path.join(__dirname, "../../public/apple-touch-icon.png"));
|
||||
await page.locator('button:text("Save")').click();
|
||||
|
||||
// About text
|
||||
await page.locator('textarea[name="about"]').fill("This is a testing org");
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
// Waiting to be in next step URL
|
||||
await page.waitForURL("/settings/organizations/*/onboard-admins");
|
||||
});
|
||||
|
||||
await test.step("On-board administrators", async () => {
|
||||
// Required field
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
// Happy path
|
||||
await page.locator('textarea[name="emails"]').fill(`rick@${orgDomain}.com`);
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
// Check if invited admin received the invitation email
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
`rick@${orgDomain}.com`,
|
||||
`${orgName}'s admin invited you to join the organization ${orgName} on Cal.com`
|
||||
);
|
||||
|
||||
// Waiting to be in next step URL
|
||||
await page.waitForURL("/settings/organizations/*/add-teams");
|
||||
});
|
||||
|
||||
await test.step("Create teams", async () => {
|
||||
// Initial state
|
||||
await expect(page.locator('input[name="teams.0.name"]')).toHaveCount(1);
|
||||
await expect(page.locator('button:text("Continue")')).toBeDisabled();
|
||||
|
||||
// Filling one team
|
||||
await page.locator('input[name="teams.0.name"]').fill("Marketing");
|
||||
await expect(page.locator('button:text("Continue")')).toBeEnabled();
|
||||
|
||||
// Adding another team
|
||||
await page.locator('button:text("Add a team")').click();
|
||||
await expect(page.locator('button:text("Continue")')).toBeDisabled();
|
||||
await expect(page.locator('input[name="teams.1.name"]')).toHaveCount(1);
|
||||
await page.locator('input[name="teams.1.name"]').fill("Sales");
|
||||
await expect(page.locator('button:text("Continue")')).toBeEnabled();
|
||||
|
||||
// Finishing the creation wizard
|
||||
await page.locator('button:text("Continue")').click();
|
||||
await page.waitForURL("/event-types");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,119 @@
|
|||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { expectInvitationEmailToBeReceived } from "./expects";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.afterEach(async ({ users, emails, clipboard }) => {
|
||||
clipboard.reset();
|
||||
await users.deleteAll();
|
||||
emails?.deleteAll();
|
||||
});
|
||||
|
||||
test.describe("Organization", () => {
|
||||
test("Invitation (non verified)", async ({ browser, page, users, emails, clipboard }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true });
|
||||
const { team: org } = await orgOwner.getOrg();
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("To the organization by email (external user)", async () => {
|
||||
const invitedUserEmail = `rick@domain-${Date.now()}.com`;
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
||||
await page.locator('button:text("Send invite")').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
const inviteLink = await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`,
|
||||
"signup?token"
|
||||
);
|
||||
|
||||
// Check newly invited member exists and is pending
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(1);
|
||||
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (!inviteLink) return null;
|
||||
|
||||
// Follow invite link in new window
|
||||
const context = await browser.newContext();
|
||||
const newPage = await context.newPage();
|
||||
newPage.goto(inviteLink);
|
||||
await newPage.waitForLoadState("networkidle");
|
||||
|
||||
// Check required fields
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await expect(newPage.locator(".text-red-700")).toHaveCount(3); // 3 password hints
|
||||
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await newPage.waitForURL("/getting-started?from=signup");
|
||||
await context.close();
|
||||
await newPage.close();
|
||||
|
||||
// Check newly invited member is not pending anymore
|
||||
await page.bringToFront();
|
||||
await page.goto("/settings/organizations/members");
|
||||
page.locator(`[data-testid="login-form"]`);
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
await test.step("To the organization by invite link", async () => {
|
||||
// Get the invite link
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator(`[data-testid="copy-invite-link-button"]`).click();
|
||||
const inviteLink = await clipboard.get();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Follow invite link in new window
|
||||
const context = await browser.newContext();
|
||||
const inviteLinkPage = await context.newPage();
|
||||
await inviteLinkPage.goto(inviteLink);
|
||||
await inviteLinkPage.waitForLoadState("networkidle");
|
||||
|
||||
// Check required fields
|
||||
await inviteLinkPage.locator("button[type=submit]").click();
|
||||
await expect(inviteLinkPage.locator(".text-red-700")).toHaveCount(4); // email + 3 password hints
|
||||
|
||||
// Happy path
|
||||
await inviteLinkPage.locator("input[name=email]").fill(`rick@domain-${Date.now()}.com`);
|
||||
await inviteLinkPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
||||
await inviteLinkPage.locator("button[type=submit]").click();
|
||||
await inviteLinkPage.waitForURL("/getting-started");
|
||||
});
|
||||
});
|
||||
|
||||
test("Invitation (verified)", async ({ browser, page, users, emails }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true });
|
||||
const { team: org } = await orgOwner.getOrg();
|
||||
await orgOwner.apiLogin();
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("To the organization by email (internal user)", async () => {
|
||||
const invitedUserEmail = `rick@example.com`;
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
||||
await page.locator('button:text("Send invite")').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`
|
||||
);
|
||||
|
||||
// Check newly invited member exists and is pending
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -77,7 +77,7 @@ test.describe("Payment app", () => {
|
|||
|
||||
await page.goto(`event-types/${paymentEvent.id}?tabName=apps`);
|
||||
await page.locator("#event-type-form").getByRole("switch").click();
|
||||
await page.locator(".text-black > .bg-default > div > div:nth-child(2)").first().click();
|
||||
await page.getByTestId("stripe-currency-select").click();
|
||||
await page.getByTestId("select-option-usd").click();
|
||||
|
||||
await page.getByTestId("price-input-stripe").click();
|
||||
|
@ -123,10 +123,10 @@ test.describe("Payment app", () => {
|
|||
await page.getByPlaceholder("Price").click();
|
||||
await page.getByPlaceholder("Price").fill("150");
|
||||
|
||||
await page.locator(".text-black > .bg-default > div > div:nth-child(2)").first().click();
|
||||
await page.getByTestId("paypal-currency-select").click();
|
||||
await page.locator("#react-select-2-option-13").click();
|
||||
|
||||
await page.locator(".mb-1 > .bg-default > div > div:nth-child(2)").first().click();
|
||||
await page.getByTestId("paypal-payment-option-select").click();
|
||||
|
||||
await page.getByText("$MXNCurrencyMexican pesoPayment option").click();
|
||||
await page.getByTestId("update-eventtype").click();
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { JSDOM } from "jsdom";
|
||||
import type { API, Messages } from "mailhog";
|
||||
|
||||
import { getEmailsReceivedByUser } from "../lib/testUtils";
|
||||
|
||||
export async function expectInvitationEmailToBeReceived(
|
||||
page: Page,
|
||||
emails: API | undefined,
|
||||
userEmail: string,
|
||||
subject: string,
|
||||
returnLink?: string
|
||||
) {
|
||||
if (!emails) return null;
|
||||
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(10000);
|
||||
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail });
|
||||
expect(receivedEmails?.total).toBe(1);
|
||||
|
||||
const [firstReceivedEmail] = (receivedEmails as Messages).items;
|
||||
|
||||
expect(firstReceivedEmail.subject).toBe(subject);
|
||||
if (!returnLink) return;
|
||||
const dom = new JSDOM(firstReceivedEmail.html);
|
||||
const anchor = dom.window.document.querySelector(`a[href*="${returnLink}"]`);
|
||||
return anchor?.getAttribute("href");
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
import { expect } from "@playwright/test";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { localize } from "../lib/testUtils";
|
||||
import { expectInvitationEmailToBeReceived } from "./expects";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.afterEach(async ({ users, emails, clipboard }) => {
|
||||
clipboard.reset();
|
||||
await users.deleteAll();
|
||||
emails?.deleteAll();
|
||||
});
|
||||
|
||||
test.describe("Team", () => {
|
||||
test("Invitation (non verified)", async ({ browser, page, users, emails, clipboard }) => {
|
||||
const t = await localize("en");
|
||||
const teamOwner = await users.create(undefined, { hasTeam: true });
|
||||
const { team } = await teamOwner.getFirstTeam();
|
||||
await teamOwner.apiLogin();
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("To the team by email (external user)", async () => {
|
||||
const invitedUserEmail = `rick_${Date.now()}@domain-${Date.now()}.com`;
|
||||
await page.locator(`button:text("${t("add")}")`).click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
||||
await page.locator(`button:text("${t("send_invite")}")`).click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
const inviteLink = await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${team.name}'s admin invited you to join the team ${team.name} on Cal.com`,
|
||||
"signup?token"
|
||||
);
|
||||
|
||||
//Check newly invited member exists and is pending
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(1);
|
||||
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (!inviteLink) return null;
|
||||
|
||||
// Follow invite link to new window
|
||||
const context = await browser.newContext();
|
||||
const newPage = await context.newPage();
|
||||
await newPage.goto(inviteLink);
|
||||
await newPage.waitForLoadState("networkidle");
|
||||
|
||||
// Check required fields
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await expect(newPage.locator('[data-testid="hint-error"]')).toHaveCount(3);
|
||||
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
||||
await newPage.locator("button[type=submit]").click();
|
||||
await newPage.waitForURL("/getting-started?from=signup");
|
||||
await newPage.close();
|
||||
await context.close();
|
||||
|
||||
// Check newly invited member is not pending anymore
|
||||
await page.bringToFront();
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
await test.step("To the team by invite link", async () => {
|
||||
const user = await users.create({
|
||||
email: `user-invite-${Date.now()}@domain.com`,
|
||||
password: "P4ssw0rd!",
|
||||
});
|
||||
await page.locator(`button:text("${t("add")}")`).click();
|
||||
await page.locator(`[data-testid="copy-invite-link-button"]`).click();
|
||||
const inviteLink = await clipboard.get();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const context = await browser.newContext();
|
||||
const inviteLinkPage = await context.newPage();
|
||||
await inviteLinkPage.goto(inviteLink);
|
||||
await inviteLinkPage.waitForLoadState("domcontentloaded");
|
||||
|
||||
await inviteLinkPage.locator("button[type=submit]").click();
|
||||
await expect(inviteLinkPage.locator('[data-testid="field-error"]')).toHaveCount(2);
|
||||
|
||||
await inviteLinkPage.locator("input[name=email]").fill(user.email);
|
||||
await inviteLinkPage.locator("input[name=password]").fill(user.username || "P4ssw0rd!");
|
||||
await inviteLinkPage.locator("button[type=submit]").click();
|
||||
|
||||
await inviteLinkPage.waitForURL(`${WEBAPP_URL}/teams**`);
|
||||
});
|
||||
});
|
||||
|
||||
test("Invitation (verified)", async ({ browser, page, users, emails }) => {
|
||||
const t = await localize("en");
|
||||
const teamOwner = await users.create({ name: `team-owner-${Date.now()}` }, { hasTeam: true });
|
||||
const { team } = await teamOwner.getFirstTeam();
|
||||
await teamOwner.apiLogin();
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await test.step("To the organization by email (internal user)", async () => {
|
||||
const invitedUserEmail = `rick@example.com`;
|
||||
await page.locator(`button:text("${t("add")}")`).click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
||||
await page.locator(`button:text("${t("send_invite")}")`).click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
invitedUserEmail,
|
||||
`${teamOwner.name} invited you to join the team ${team.name} on Cal.com`
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
||||
).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -351,7 +351,7 @@ test.describe("Teams - Org", () => {
|
|||
|
||||
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||
|
||||
await expect(page.locator('[data-testid="404-page"]')).toBeVisible();
|
||||
await expect(page.locator("text=This page could not be found")).toBeVisible();
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
|
|
|
@ -1098,6 +1098,7 @@
|
|||
"developer_documentation": "Documentation pour développeurs",
|
||||
"get_in_touch": "Contactez-nous",
|
||||
"contact_support": "Contacter l'assistance",
|
||||
"premium_support": "Assistance Premium",
|
||||
"community_support": "Aide communautaire",
|
||||
"feedback": "Commentaires",
|
||||
"submitted_feedback": "Merci pour vos commentaires !",
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"paths": {
|
||||
"~/*": ["modules/*"],
|
||||
"@components/*": ["components/*"],
|
||||
"@pages/*": ["pages/*"],
|
||||
"@lib/*": ["lib/*"],
|
||||
"@server/*": ["server/*"],
|
||||
"@prisma/client/*": ["@calcom/prisma/client/*"]
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
"@playwright/test": "^1.31.2",
|
||||
"@snaplet/copycat": "^0.3.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@types/jsdom": "^21.1.3",
|
||||
"@types/jsonwebtoken": "^9.0.3",
|
||||
"c8": "^7.13.0",
|
||||
"checkly": "latest",
|
||||
|
|
|
@ -40,6 +40,8 @@ export const withPaidAppRedirect = async ({
|
|||
? {
|
||||
subscription_data: {
|
||||
trial_period_days: trialDays,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - trial_settings isn't available cc @erik
|
||||
trial_settings: { end_behavior: { missing_payment_method: "cancel" } },
|
||||
},
|
||||
}
|
||||
|
|
|
@ -72,7 +72,10 @@ export class PaymentService implements IAbstractPaymentService {
|
|||
amount: payment.amount,
|
||||
externalId: invoice.paymentRequest,
|
||||
currency: payment.currency,
|
||||
data: Object.assign({}, { invoice }) as unknown as Prisma.InputJsonValue,
|
||||
data: Object.assign(
|
||||
{},
|
||||
{ invoice: { ...invoice, isPaid: false } }
|
||||
) as unknown as Prisma.InputJsonValue,
|
||||
fee: 0,
|
||||
refunded: false,
|
||||
success: false,
|
||||
|
@ -84,7 +87,7 @@ export class PaymentService implements IAbstractPaymentService {
|
|||
}
|
||||
return paymentData;
|
||||
} catch (error) {
|
||||
log.error("Alby: Payment could not be created", bookingId);
|
||||
log.error("Alby: Payment could not be created", bookingId, JSON.stringify(error));
|
||||
throw new Error(ErrorCode.PaymentCreationFailure);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
"__template": "basic",
|
||||
"dirName": "cal-ai",
|
||||
"paid": {
|
||||
"priceInUsd": 25,
|
||||
"priceInUsd": 8,
|
||||
"priceId": "price_1O1ziDH8UDiwIftkDHp3MCTP",
|
||||
"mode": "subscription"
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import { google } from "googleapis";
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
|
@ -14,28 +16,30 @@ const scopes = [
|
|||
let client_id = "";
|
||||
let client_secret = "";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "GET") {
|
||||
// Get token from Google Calendar API
|
||||
const appKeys = await getAppKeysFromSlug("google-calendar");
|
||||
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||
if (!client_id) return res.status(400).json({ message: "Google client_id missing." });
|
||||
if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." });
|
||||
const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Get token from Google Calendar API
|
||||
const appKeys = await getAppKeysFromSlug("google-calendar");
|
||||
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||
if (!client_id) throw new HttpError({ statusCode: 400, message: "Google client_id missing." });
|
||||
if (!client_secret) throw new HttpError({ statusCode: 400, message: "Google client_secret missing." });
|
||||
const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
|
||||
const authUrl = oAuth2Client.generateAuthUrl({
|
||||
access_type: "offline",
|
||||
scope: scopes,
|
||||
// A refresh token is only returned the first time the user
|
||||
// consents to providing access. For illustration purposes,
|
||||
// setting the prompt to 'consent' will force this consent
|
||||
// every time, forcing a refresh_token to be returned.
|
||||
prompt: "consent",
|
||||
state: encodeOAuthState(req),
|
||||
});
|
||||
const authUrl = oAuth2Client.generateAuthUrl({
|
||||
access_type: "offline",
|
||||
scope: scopes,
|
||||
// A refresh token is only returned the first time the user
|
||||
// consents to providing access. For illustration purposes,
|
||||
// setting the prompt to 'consent' will force this consent
|
||||
// every time, forcing a refresh_token to be returned.
|
||||
prompt: "consent",
|
||||
state: encodeOAuthState(req),
|
||||
});
|
||||
|
||||
res.status(200).json({ url: authUrl });
|
||||
}
|
||||
res.status(200).json({ url: authUrl });
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
});
|
||||
|
|
|
@ -3,6 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
|
||||
import { WEBAPP_URL_FOR_OAUTH, CAL_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
|
@ -12,24 +14,23 @@ import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
|||
let client_id = "";
|
||||
let client_secret = "";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { code } = req.query;
|
||||
const state = decodeOAuthState(req);
|
||||
|
||||
if (typeof code !== "string") {
|
||||
res.status(400).json({ message: "`code` must be a string" });
|
||||
return;
|
||||
throw new HttpError({ statusCode: 400, message: "`code` must be a string" });
|
||||
}
|
||||
|
||||
if (!req.session?.user?.id) {
|
||||
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||
throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" });
|
||||
}
|
||||
|
||||
const appKeys = await getAppKeysFromSlug("google-calendar");
|
||||
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||
if (!client_id) return res.status(400).json({ message: "Google client_id missing." });
|
||||
if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." });
|
||||
if (!client_id) throw new HttpError({ statusCode: 400, message: "Google client_id missing." });
|
||||
if (!client_secret) throw new HttpError({ statusCode: 400, message: "Google client_secret missing." });
|
||||
|
||||
const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;
|
||||
|
||||
|
@ -107,3 +108,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
getInstalledAppPath({ variant: "calendar", slug: "google-calendar" })
|
||||
);
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
});
|
||||
|
|
|
@ -78,17 +78,15 @@ test("Calendar Cache is being called", async () => {
|
|||
|
||||
// prismaMock.calendarCache.create.mock.
|
||||
const calendarService = new CalendarService(testCredential);
|
||||
// @ts-expect-error authedCalendar is a private method, hence the TS error
|
||||
vi.spyOn(calendarService, "authedCalendar").mockReturnValue(
|
||||
// @ts-expect-error trust me bro
|
||||
{
|
||||
freebusy: {
|
||||
query: vi.fn().mockReturnValue({
|
||||
data: testFreeBusyResponse,
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
vi.spyOn(calendarService, "authedCalendar").mockReturnValue({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore - Mocking the authedCalendar so can't return the actual response
|
||||
freebusy: {
|
||||
query: vi.fn().mockReturnValue({
|
||||
data: testFreeBusyResponse,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await calendarService.getAvailability(new Date().toISOString(), new Date().toISOString(), [
|
||||
testSelectedCalendar,
|
||||
|
|
|
@ -132,7 +132,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||
};
|
||||
};
|
||||
|
||||
private authedCalendar = async () => {
|
||||
public authedCalendar = async () => {
|
||||
const myGoogleAuth = await this.auth.getToken();
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
import { expect } from "@playwright/test";
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { test } from "@calcom/web/playwright/lib/fixtures";
|
||||
import { selectSecondAvailableTimeSlotNextMonth } from "@calcom/web/playwright/lib/testUtils";
|
||||
|
||||
import metadata from "../_metadata";
|
||||
import GoogleCalendarService from "../lib/CalendarService";
|
||||
import { createBookingAndFetchGCalEvent, deleteBookingAndEvent, assertValueExists } from "./testUtils";
|
||||
|
||||
test.describe("Google Calendar", async () => {
|
||||
test.describe("Test using the primary calendar", async () => {
|
||||
let qaUsername: string;
|
||||
let qaGCalCredential: Prisma.CredentialGetPayload<{ select: { id: true } }>;
|
||||
test.beforeAll(async () => {
|
||||
let runIntegrationTest = false;
|
||||
|
||||
test.skip(!!APP_CREDENTIAL_SHARING_ENABLED, "Credential sharing enabled");
|
||||
|
||||
if (process.env.E2E_TEST_CALCOM_QA_EMAIL && process.env.E2E_TEST_CALCOM_QA_PASSWORD) {
|
||||
qaGCalCredential = await prisma.credential.findFirstOrThrow({
|
||||
where: {
|
||||
user: {
|
||||
email: process.env.E2E_TEST_CALCOM_QA_EMAIL,
|
||||
},
|
||||
type: metadata.type,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
const qaUserQuery = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
email: process.env.E2E_TEST_CALCOM_QA_EMAIL,
|
||||
},
|
||||
select: {
|
||||
username: true,
|
||||
},
|
||||
});
|
||||
|
||||
assertValueExists(qaUserQuery.username, "qaUsername");
|
||||
qaUsername = qaUserQuery.username;
|
||||
|
||||
if (qaGCalCredential && qaUsername) runIntegrationTest = true;
|
||||
}
|
||||
|
||||
test.skip(!runIntegrationTest, "QA user not found");
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
assertValueExists(process.env.E2E_TEST_CALCOM_QA_EMAIL, "qaEmail");
|
||||
|
||||
const qaUserStore = await users.set(process.env.E2E_TEST_CALCOM_QA_EMAIL);
|
||||
|
||||
await qaUserStore.apiLogin(process.env.E2E_TEST_CALCOM_QA_PASSWORD);
|
||||
|
||||
// Need to refresh keys from DB
|
||||
const refreshedCredential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
id: qaGCalCredential?.id,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
assertValueExists(refreshedCredential, "refreshedCredential");
|
||||
|
||||
const googleCalendarService = new GoogleCalendarService(refreshedCredential);
|
||||
|
||||
const calendars = await googleCalendarService.listCalendars();
|
||||
|
||||
const primaryCalendarName = calendars.find((calendar) => calendar.primary)?.name;
|
||||
assertValueExists(primaryCalendarName, "primaryCalendarName");
|
||||
|
||||
await page.goto("/apps/installed/calendar");
|
||||
|
||||
await page.waitForSelector('[title*="Create events on"]');
|
||||
await page.locator('[title*="Create events on"]').locator("svg").click();
|
||||
await page.locator("#react-select-2-option-0-0").getByText(primaryCalendarName).click();
|
||||
});
|
||||
|
||||
test("On new booking, event should be created on GCal", async ({ page }) => {
|
||||
const { gCalEvent, gCalReference, booking, authedCalendar } = await createBookingAndFetchGCalEvent(
|
||||
page as Page,
|
||||
qaGCalCredential,
|
||||
qaUsername
|
||||
);
|
||||
|
||||
assertValueExists(gCalEvent.start?.timeZone, "gCalEvent");
|
||||
assertValueExists(gCalEvent.end?.timeZone, "gCalEvent");
|
||||
|
||||
// Ensure that the start and end times are matching
|
||||
const startTimeMatches = dayjs(booking.startTime).isSame(
|
||||
dayjs(gCalEvent.start.dateTime).tz(gCalEvent.start.timeZone)
|
||||
);
|
||||
const endTimeMatches = dayjs(booking.endTime).isSame(
|
||||
dayjs(gCalEvent.end?.dateTime).tz(gCalEvent.end.timeZone)
|
||||
);
|
||||
expect(startTimeMatches && endTimeMatches).toBe(true);
|
||||
|
||||
// Ensure that the titles are matching
|
||||
expect(booking.title).toBe(gCalEvent.summary);
|
||||
|
||||
// Ensure that the attendee is on the event
|
||||
const bookingAttendee = booking?.attendees[0].email;
|
||||
const attendeeInGCalEvent = gCalEvent.attendees?.find((attendee) => attendee.email === bookingAttendee);
|
||||
expect(attendeeInGCalEvent).toBeTruthy();
|
||||
|
||||
await deleteBookingAndEvent(authedCalendar, booking.uid, gCalReference.uid);
|
||||
});
|
||||
|
||||
test("On reschedule, event should be updated on GCal", async ({ page }) => {
|
||||
// Reschedule the booking and check the gCalEvent's time is also changed
|
||||
// On reschedule gCal UID stays the same
|
||||
const { gCalReference, booking, authedCalendar } = await createBookingAndFetchGCalEvent(
|
||||
page,
|
||||
qaGCalCredential,
|
||||
qaUsername
|
||||
);
|
||||
|
||||
await page.locator('[data-testid="reschedule-link"]').click();
|
||||
|
||||
await selectSecondAvailableTimeSlotNextMonth(page);
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
const rescheduledBookingUrl = await page.url();
|
||||
const rescheduledBookingUid = rescheduledBookingUrl.match(/booking\/([^\/?]+)/);
|
||||
|
||||
assertValueExists(rescheduledBookingUid, "rescheduledBookingUid");
|
||||
|
||||
// Get the rescheduled booking start and end times
|
||||
const rescheduledBooking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: rescheduledBookingUid[1],
|
||||
},
|
||||
select: {
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
},
|
||||
});
|
||||
assertValueExists(rescheduledBooking, "rescheduledBooking");
|
||||
|
||||
// The GCal event UID persists after reschedule but should get the rescheduled data
|
||||
const gCalRescheduledEventResponse = await authedCalendar.events.get({
|
||||
calendarId: "primary",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
eventId: gCalReference.uid,
|
||||
});
|
||||
|
||||
expect(gCalRescheduledEventResponse.status).toBe(200);
|
||||
|
||||
const rescheduledGCalEvent = gCalRescheduledEventResponse.data;
|
||||
|
||||
assertValueExists(rescheduledGCalEvent.start?.timeZone, "rescheduledGCalEvent");
|
||||
assertValueExists(rescheduledGCalEvent.end?.timeZone, "rescheduledGCalEvent");
|
||||
|
||||
// Ensure that the new start and end times are matching
|
||||
const rescheduledStartTimeMatches = dayjs(rescheduledBooking.startTime).isSame(
|
||||
dayjs(rescheduledGCalEvent.start?.dateTime).tz(rescheduledGCalEvent.start?.timeZone)
|
||||
);
|
||||
const rescheduledEndTimeMatches = dayjs(rescheduledBooking.endTime).isSame(
|
||||
dayjs(rescheduledGCalEvent.end?.dateTime).tz(rescheduledGCalEvent.end.timeZone)
|
||||
);
|
||||
expect(rescheduledStartTimeMatches && rescheduledEndTimeMatches).toBe(true);
|
||||
|
||||
// After test passes we can delete the bookings and GCal event
|
||||
await deleteBookingAndEvent(authedCalendar, booking.uid, gCalReference.uid);
|
||||
|
||||
await prisma.booking.delete({
|
||||
where: {
|
||||
uid: rescheduledBookingUid[1],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("When canceling the booking, the GCal event should also be deleted", async ({ page }) => {
|
||||
const { gCalReference, booking, authedCalendar } = await createBookingAndFetchGCalEvent(
|
||||
page,
|
||||
qaGCalCredential,
|
||||
qaUsername
|
||||
);
|
||||
|
||||
// Cancel the booking
|
||||
await page.locator('[data-testid="cancel"]').click();
|
||||
await page.locator('[data-testid="confirm_cancel"]').click();
|
||||
// Query for the bookingUID and ensure that it doesn't exist on GCal
|
||||
|
||||
await page.waitForSelector('[data-testid="cancelled-headline"]');
|
||||
|
||||
const canceledGCalEventResponse = await authedCalendar.events.get({
|
||||
calendarId: "primary",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
eventId: gCalReference.uid,
|
||||
});
|
||||
|
||||
expect(canceledGCalEventResponse.data.status).toBe("cancelled");
|
||||
|
||||
// GCal API sees canceled events as already deleted
|
||||
await deleteBookingAndEvent(authedCalendar, booking.uid);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,127 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { bookFirstEvent } from "@calcom/web/playwright/lib/testUtils";
|
||||
|
||||
import metadata from "../_metadata";
|
||||
import GoogleCalendarService from "../lib/CalendarService";
|
||||
|
||||
/**
|
||||
* Creates the booking on Cal.com and makes the GCal call to fetch the event.
|
||||
* Ends on the booking success page
|
||||
* @param page
|
||||
*
|
||||
* @returns the raw GCal event GET response and the booking reference
|
||||
*/
|
||||
export const createBookingAndFetchGCalEvent = async (
|
||||
page: Page,
|
||||
qaGCalCredential: Prisma.CredentialGetPayload<{ select: { id: true } }> | null,
|
||||
qaUsername: string
|
||||
) => {
|
||||
await page.goto(`/${qaUsername}`);
|
||||
await bookFirstEvent(page);
|
||||
|
||||
const bookingUrl = await page.url();
|
||||
const bookingUid = bookingUrl.match(/booking\/([^\/?]+)/);
|
||||
assertValueExists(bookingUid, "bookingUid");
|
||||
|
||||
const [gCalReference, booking] = await Promise.all([
|
||||
prisma.bookingReference.findFirst({
|
||||
where: {
|
||||
booking: {
|
||||
uid: bookingUid[1],
|
||||
},
|
||||
type: metadata.type,
|
||||
credentialId: qaGCalCredential?.id,
|
||||
},
|
||||
select: {
|
||||
uid: true,
|
||||
booking: {},
|
||||
},
|
||||
}),
|
||||
prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: bookingUid[1],
|
||||
},
|
||||
select: {
|
||||
uid: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
title: true,
|
||||
attendees: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
assertValueExists(gCalReference, "gCalReference");
|
||||
assertValueExists(booking, "booking");
|
||||
|
||||
// Need to refresh keys from DB
|
||||
const refreshedCredential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
id: qaGCalCredential?.id,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(refreshedCredential).toBeTruthy();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
const googleCalendarService = new GoogleCalendarService(refreshedCredential);
|
||||
|
||||
const authedCalendar = await googleCalendarService.authedCalendar();
|
||||
|
||||
const gCalEventResponse = await authedCalendar.events.get({
|
||||
calendarId: "primary",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
eventId: gCalReference.uid,
|
||||
});
|
||||
|
||||
expect(gCalEventResponse.status).toBe(200);
|
||||
|
||||
return { gCalEvent: gCalEventResponse.data, gCalReference, booking, authedCalendar };
|
||||
};
|
||||
|
||||
export const deleteBookingAndEvent = async (
|
||||
authedCalendar: any,
|
||||
bookingUid: string,
|
||||
gCalReferenceUid?: string
|
||||
) => {
|
||||
// After test passes we can delete the booking and GCal event
|
||||
await prisma.booking.delete({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
});
|
||||
|
||||
if (gCalReferenceUid) {
|
||||
await authedCalendar.events.delete({
|
||||
calendarId: "primary",
|
||||
eventId: gCalReferenceUid,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function assertValueExists(value: unknown, variableName?: string): asserts value {
|
||||
if (!value) {
|
||||
throw new Error(`Value is not defined: ${variableName}`);
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
response_type: "code",
|
||||
scope: scopes.join(" "),
|
||||
client_id,
|
||||
prompt: "select_account",
|
||||
redirect_uri: `${WEBAPP_URL}/api/integrations/office365calendar/callback`,
|
||||
state,
|
||||
};
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"lodash": "^4.17.21",
|
||||
"qs-stringify": "^1.2.1",
|
||||
"react-i18next": "^12.2.0",
|
||||
"stripe": "^14.3.0"
|
||||
"stripe": "^9.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
|
|
|
@ -92,6 +92,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||
</label>
|
||||
<Select
|
||||
variant="default"
|
||||
data-testid="paypal-currency-select"
|
||||
options={currencyOptions}
|
||||
value={selectedCurrency}
|
||||
className="text-black"
|
||||
|
@ -111,6 +112,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||
Payment option
|
||||
</label>
|
||||
<Select<Option>
|
||||
data-testid="paypal-payment-option-select"
|
||||
defaultValue={
|
||||
paymentOptionSelectValue
|
||||
? { ...paymentOptionSelectValue, label: t(paymentOptionSelectValue.label) }
|
||||
|
|
|
@ -36,7 +36,7 @@ test.describe("Routing Forms", () => {
|
|||
await page.goto(`apps/routing-forms/route-builder/${formId}`);
|
||||
await disableForm(page);
|
||||
await gotoRoutingLink({ page, formId });
|
||||
await expect(page.locator("text=ERROR 404")).toBeVisible();
|
||||
await expect(page.locator("text=This page could not be found")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should be able to edit the form", async ({ page }) => {
|
||||
|
|
|
@ -100,7 +100,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||
{t("currency")}
|
||||
</label>
|
||||
<Select
|
||||
data-testid="currency-select-stripe"
|
||||
data-testid="stripe-currency-select"
|
||||
variant="default"
|
||||
options={currencyOptions}
|
||||
value={selectedCurrency}
|
||||
|
@ -119,6 +119,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||
Payment option
|
||||
</label>
|
||||
<Select<Option>
|
||||
data-testid="stripe-payment-option-select"
|
||||
defaultValue={
|
||||
paymentOptionSelectValue
|
||||
? { ...paymentOptionSelectValue, label: t(paymentOptionSelectValue.label) }
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"prettier": "^2.8.6",
|
||||
"prettier-plugin-tailwindcss": "^0.2.5",
|
||||
"tailwind-scrollbar": "^2.0.1",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ class CalendarEventClass implements CalendarEvent {
|
|||
organizer!: Person;
|
||||
attendees!: Person[];
|
||||
description?: string | null;
|
||||
team?: { name: string; members: Person[] };
|
||||
team?: { name: string; members: Person[]; id: number };
|
||||
location?: string | null;
|
||||
conferenceData?: ConferenceData;
|
||||
additionalInformation?: AdditionalInformation;
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
"autoprefixer": "^10.4.12",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.18",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.1.2"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
"use client";
|
||||
|
||||
import type { ComponentProps } from "react";
|
||||
import React from "react";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
|
||||
export default function MainLayout({
|
||||
children,
|
||||
...rest
|
||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||
return (
|
||||
<Shell withoutMain={true} {...rest}>
|
||||
{children}
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
export const getLayout = (page: React.ReactElement) => <MainLayout>{page}</MainLayout>;
|
|
@ -2,9 +2,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";
|
||||
import { type ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
||||
import { type ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
|
||||
|
||||
//@ts-expect-error no type definitions
|
||||
import { i18n } from "@calcom/web/next-i18next.config";
|
||||
import { i18n } from "@calcom/config/next-i18next.config";
|
||||
|
||||
/**
|
||||
* This is a slimmed down version of the `getServerSession` function from
|
||||
|
@ -17,9 +19,16 @@ import { i18n } from "@calcom/web/next-i18next.config";
|
|||
* token has expired (30 days). This should be fine as we call `/auth/session`
|
||||
* frequently enough on the client-side to keep the session alive.
|
||||
*/
|
||||
export const getLocale = async (req: GetTokenParams["req"]): Promise<string> => {
|
||||
export const getLocale = async (
|
||||
req:
|
||||
| GetTokenParams["req"]
|
||||
| {
|
||||
cookies: ReadonlyRequestCookies;
|
||||
headers: ReadonlyHeaders;
|
||||
}
|
||||
): Promise<string> => {
|
||||
const token = await getToken({
|
||||
req,
|
||||
req: req as GetTokenParams["req"],
|
||||
});
|
||||
|
||||
const tokenLocale = token?.["locale"];
|
||||
|
|
|
@ -72,7 +72,12 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
|
|||
hideBranding: true,
|
||||
},
|
||||
},
|
||||
teamId: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
recurringEvent: true,
|
||||
title: true,
|
||||
eventName: true,
|
||||
|
@ -151,11 +156,10 @@ async function handler(req: CustomRequest) {
|
|||
|
||||
const teamId = await getTeamIdFromEventType({
|
||||
eventType: {
|
||||
team: { id: bookingToDelete.eventType?.teamId ?? null },
|
||||
team: { id: bookingToDelete.eventType?.team?.id ?? null },
|
||||
parentId: bookingToDelete?.eventType?.parentId ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
const triggerForUser = !teamId || (teamId && bookingToDelete.eventType?.parentId);
|
||||
|
||||
const subscriberOptions = {
|
||||
|
@ -227,6 +231,7 @@ async function handler(req: CustomRequest) {
|
|||
type: bookingToDelete?.eventType?.slug as string,
|
||||
description: bookingToDelete?.description || "",
|
||||
customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs),
|
||||
eventTypeId: bookingToDelete.eventTypeId as number,
|
||||
...getCalEventResponses({
|
||||
bookingFields: bookingToDelete.eventType?.bookingFields ?? null,
|
||||
booking: bookingToDelete,
|
||||
|
@ -254,7 +259,9 @@ async function handler(req: CustomRequest) {
|
|||
? [bookingToDelete?.user.destinationCalendar]
|
||||
: [],
|
||||
cancellationReason: cancellationReason,
|
||||
...(teamMembers && { team: { name: "", members: teamMembers } }),
|
||||
...(teamMembers && {
|
||||
team: { name: bookingToDelete?.eventType?.team?.name || "Nameless", members: teamMembers, id: teamId! },
|
||||
}),
|
||||
seatsPerTimeSlot: bookingToDelete.eventType?.seatsPerTimeSlot,
|
||||
seatsShowAttendees: bookingToDelete.eventType?.seatsShowAttendees,
|
||||
};
|
||||
|
@ -407,7 +414,7 @@ async function handler(req: CustomRequest) {
|
|||
if (bookingToDelete.location === DailyLocationType) {
|
||||
bookingToDelete.user.credentials.push({
|
||||
...FAKE_DAILY_CREDENTIAL,
|
||||
teamId: bookingToDelete.eventType?.teamId || null,
|
||||
teamId: bookingToDelete.eventType?.team?.id || null,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -539,10 +546,10 @@ async function handler(req: CustomRequest) {
|
|||
let eventTypeOwnerId;
|
||||
if (bookingToDelete.eventType?.owner) {
|
||||
eventTypeOwnerId = bookingToDelete.eventType.owner.id;
|
||||
} else if (bookingToDelete.eventType?.teamId) {
|
||||
} else if (bookingToDelete.eventType?.team?.id) {
|
||||
const teamOwner = await prisma.membership.findFirst({
|
||||
where: {
|
||||
teamId: bookingToDelete.eventType.teamId,
|
||||
teamId: bookingToDelete.eventType?.team.id,
|
||||
role: MembershipRole.OWNER,
|
||||
},
|
||||
select: {
|
||||
|
|
|
@ -1001,8 +1001,11 @@ async function handler(
|
|||
const attendeeTimezone = attendeeInfoOnReschedule ? attendeeInfoOnReschedule.timeZone : reqBody.timeZone;
|
||||
|
||||
const tAttendees = await getTranslation(attendeeLanguage ?? "en", "common");
|
||||
|
||||
const isManagedEventType = !!eventType.parentId;
|
||||
|
||||
// use host default
|
||||
if (isTeamEventType && locationBodyString === OrganizerDefaultConferencingAppType) {
|
||||
if ((isManagedEventType || isTeamEventType) && locationBodyString === OrganizerDefaultConferencingAppType) {
|
||||
const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata);
|
||||
const organizerMetadata = metadataParseResult.success ? metadataParseResult.data : undefined;
|
||||
if (organizerMetadata?.defaultConferencingApp?.appSlug) {
|
||||
|
@ -1076,7 +1079,6 @@ async function handler(
|
|||
},
|
||||
};
|
||||
});
|
||||
|
||||
const teamMembers = await Promise.all(teamMemberPromises);
|
||||
|
||||
const attendeesList = [...invitee, ...guests];
|
||||
|
@ -1884,6 +1886,7 @@ async function handler(
|
|||
evt.team = {
|
||||
members: teamMembers,
|
||||
name: eventType.team?.name || "Nameless",
|
||||
id: eventType.team?.id ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,24 +11,6 @@ const MembersView = () => {
|
|||
<LicenseRequired>
|
||||
<Meta title={t("organization_members")} description={t("organization_description")} />
|
||||
<div>
|
||||
{/* {team && (
|
||||
<>
|
||||
{isInviteOpen && (
|
||||
<TeamInviteList
|
||||
teams={[
|
||||
{
|
||||
id: team.id,
|
||||
accepted: team.membership.accepted || false,
|
||||
logo: team.logo,
|
||||
name: team.name,
|
||||
slug: team.slug,
|
||||
role: team.membership.role,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)} */}
|
||||
<UserListTable />
|
||||
</div>
|
||||
</LicenseRequired>
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
TextAreaField,
|
||||
} from "@calcom/ui";
|
||||
import { Link } from "@calcom/ui/components/icon";
|
||||
import type { Window as WindowWithClipboardValue } from "@calcom/web/playwright/fixtures/clipboard";
|
||||
|
||||
import type { PendingMember } from "../lib/types";
|
||||
import { GoogleWorkspaceInviteButton } from "./GoogleWorkspaceInviteButton";
|
||||
|
@ -92,8 +93,15 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
|||
|
||||
const inviteLink =
|
||||
isOrgInvite || (props?.orgMembers && props.orgMembers?.length > 0) ? orgInviteLink : teamInviteLink;
|
||||
await navigator.clipboard.writeText(inviteLink);
|
||||
showToast(t("invite_link_copied"), "success");
|
||||
try {
|
||||
await navigator.clipboard.writeText(inviteLink);
|
||||
showToast(t("invite_link_copied"), "success");
|
||||
} catch (e) {
|
||||
if (process.env.NEXT_PUBLIC_IS_E2E) {
|
||||
(window as WindowWithClipboardValue).E2E_CLIPBOARD_VALUE = inviteLink;
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const options: MembershipRoleOption[] = useMemo(() => {
|
||||
|
|
|
@ -152,7 +152,14 @@ export default function MemberListItem(props: Props) {
|
|||
{props.member.role && <TeamRole role={props.member.role} />}
|
||||
</div>
|
||||
<div className="text-default flex items-center">
|
||||
<span className=" block text-sm" data-testid="member-email" data-email={props.member.email}>
|
||||
<span
|
||||
className=" block text-sm"
|
||||
data-testid={
|
||||
props.member.accepted
|
||||
? "member-email"
|
||||
: `email-${props.member.email.replace("@", "")}-pending`
|
||||
}
|
||||
data-email={props.member.email}>
|
||||
{props.member.email}
|
||||
</span>
|
||||
{bookingLink && (
|
||||
|
|
|
@ -106,6 +106,7 @@ export const ChildrenEventTypeSelect = ({
|
|||
{children.created && children.owner.username && (
|
||||
<Tooltip content={t("preview")}>
|
||||
<Button
|
||||
data-testid="preview-button"
|
||||
color="secondary"
|
||||
target="_blank"
|
||||
variant="icon"
|
||||
|
|
|
@ -125,6 +125,7 @@ export default function CreateEventTypeDialog({
|
|||
}),
|
||||
"success"
|
||||
);
|
||||
form.reset();
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
|
|
|
@ -422,6 +422,7 @@ function FieldEditDialog({
|
|||
<DialogHeader title={t("add_a_booking_question")} subtitle={t("booking_questions_description")} />
|
||||
<SelectField
|
||||
defaultValue={fieldTypesConfigMap.text}
|
||||
data-testid="test-field-type"
|
||||
id="test-field-type"
|
||||
isDisabled={
|
||||
fieldForm.getValues("editable") === "system" ||
|
||||
|
|
|
@ -5,7 +5,6 @@ import type { Dayjs } from "@calcom/dayjs";
|
|||
import dayjs from "@calcom/dayjs";
|
||||
import { yyyymmdd } from "@calcom/lib/date-fns";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import type { WorkingHours } from "@calcom/types/schedule";
|
||||
import {
|
||||
Dialog,
|
||||
|
@ -210,19 +209,12 @@ const DateOverrideInputDialog = ({
|
|||
onChange: (newValue: TimeRange[]) => void;
|
||||
value?: TimeRange[];
|
||||
}) => {
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
const [open, setOpen] = useState(false);
|
||||
{
|
||||
/* enableOverflow is used to allow overflow when there are too many overrides to show on mobile.
|
||||
ref:- https://github.com/calcom/cal.com/pull/6215
|
||||
*/
|
||||
}
|
||||
const enableOverflow = isMobile;
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{Trigger}</DialogTrigger>
|
||||
|
||||
<DialogContent enableOverflow={enableOverflow} size="md" className="p-0">
|
||||
<DialogContent enableOverflow={true} size="md" className="p-0">
|
||||
<DateOverrideForm
|
||||
excludedDates={excludedDates}
|
||||
{...passThroughProps}
|
||||
|
|
|
@ -96,7 +96,7 @@ export const tips = [
|
|||
{
|
||||
id: 12,
|
||||
thumbnailUrl:
|
||||
"https://ph-files.imgix.net/46d376e1-f897-40fc-9921-c64de971ee13.jpeg?auto=compress&codec=mozjpeg&cs=strip&auto=format&w=390&h=220&fit=max&dpr=2",
|
||||
"https://cal.com/og-image-cal-ai.jpg",
|
||||
mediaLink: "https://go.cal.com/cal-ai",
|
||||
title: "Cal.ai",
|
||||
description: "Your personal AI scheduling assistant",
|
||||
|
|
|
@ -204,12 +204,15 @@ export function UserListTable() {
|
|||
id: "teams",
|
||||
header: "Teams",
|
||||
cell: ({ row }) => {
|
||||
const { teams, accepted } = row.original;
|
||||
const { teams, accepted, email } = row.original;
|
||||
// TODO: Implement click to filter
|
||||
return (
|
||||
<div className="flex h-full flex-wrap items-center gap-2">
|
||||
{accepted ? null : (
|
||||
<Badge variant="red" className="text-xs">
|
||||
<Badge
|
||||
variant="red"
|
||||
className="text-xs"
|
||||
data-testid={`email-${email.replace("@", "")}-pending`}>
|
||||
Pending
|
||||
</Badge>
|
||||
)}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Badge, Button, showToast } from "@calcom/ui";
|
|||
import { Activity } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function WebhookTestDisclosure() {
|
||||
const subscriberUrl: string = useWatch({ name: "subscriberUrl" });
|
||||
const [subscriberUrl, webhookSecret]: [string, string] = useWatch({ name: ["subscriberUrl", "secret"] });
|
||||
const payloadTemplate = useWatch({ name: "payloadTemplate" }) || null;
|
||||
const { t } = useLocale();
|
||||
const mutation = trpc.viewer.webhook.testTrigger.useMutation({
|
||||
|
@ -27,7 +27,9 @@ export default function WebhookTestDisclosure() {
|
|||
color="secondary"
|
||||
disabled={mutation.isLoading || !subscriberUrl}
|
||||
StartIcon={Activity}
|
||||
onClick={() => mutation.mutate({ url: subscriberUrl, type: "PING", payloadTemplate })}>
|
||||
onClick={() =>
|
||||
mutation.mutate({ url: subscriberUrl, secret: webhookSecret, type: "PING", payloadTemplate })
|
||||
}>
|
||||
{t("ping_test")}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -106,4 +106,11 @@ export const APP_CREDENTIAL_SHARING_ENABLED =
|
|||
|
||||
export const DEFAULT_LIGHT_BRAND_COLOR = "#292929";
|
||||
export const DEFAULT_DARK_BRAND_COLOR = "#fafafa";
|
||||
export const AB_TEST_BUCKET_PROBABILITY = Number(process.env.AB_TEST_BUCKET_PROBABILITY ?? "10");
|
||||
|
||||
const defaultOnNaN = (testedValue: number, defaultValue: number) =>
|
||||
!Number.isNaN(testedValue) ? testedValue : defaultValue;
|
||||
|
||||
export const AB_TEST_BUCKET_PROBABILITY = defaultOnNaN(
|
||||
parseInt(process.env.AB_TEST_BUCKET_PROBABILITY ?? "10", 10),
|
||||
10
|
||||
);
|
||||
|
|
|
@ -8,7 +8,9 @@ export const useCompatSearchParams = () => {
|
|||
Object.getOwnPropertyNames(params).forEach((key) => {
|
||||
searchParams.delete(key);
|
||||
|
||||
const param = params[key];
|
||||
// Though useParams is supposed to return a string/string[] as the key's value but it is found to return undefined as well.
|
||||
// Maybe it happens for pages dir when using optional catch-all routes.
|
||||
const param = params[key] || "";
|
||||
const paramArr = typeof param === "string" ? param.split("/") : param;
|
||||
|
||||
paramArr.forEach((p) => {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import i18next from "i18next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
//@ts-expect-error no type definitions
|
||||
import config from "@calcom/web/next-i18next.config";
|
||||
|
||||
export const create = async (locale: string, ns: string) => {
|
||||
const { _nextI18Next } = await serverSideTranslations(locale, [ns], config);
|
||||
|
||||
const _i18n = i18next.createInstance();
|
||||
_i18n.init({
|
||||
lng: locale,
|
||||
resources: _nextI18Next?.initialI18nStore,
|
||||
fallbackLng: _nextI18Next?.userConfig?.i18n.defaultLocale,
|
||||
});
|
||||
return _i18n;
|
||||
};
|
||||
|
||||
export const getFixedT = async (locale: string, ns: string) => {
|
||||
const i18n = await create(locale, ns);
|
||||
|
||||
return i18n.getFixedT(locale, ns);
|
||||
};
|
|
@ -455,6 +455,22 @@ async function main() {
|
|||
},
|
||||
});
|
||||
|
||||
await createUserAndEventType({
|
||||
user: {
|
||||
email: process.env.E2E_TEST_CALCOM_QA_EMAIL || "qa@example.com",
|
||||
password: process.env.E2E_TEST_CALCOM_QA_PASSWORD || "qa",
|
||||
username: "qa",
|
||||
name: "QA Example",
|
||||
},
|
||||
eventTypes: [
|
||||
{
|
||||
title: "15min",
|
||||
slug: "15min",
|
||||
length: 15,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await createTeamAndAddUsers(
|
||||
{
|
||||
name: "Seeded Team",
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import z from "zod";
|
||||
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
|
@ -328,52 +327,37 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp
|
|||
}
|
||||
}
|
||||
|
||||
// Backwards compatibility. Selected calendars cascade on delete when deleting a credential
|
||||
// If it's a calendar remove it from the SelectedCalendars
|
||||
if (credential.app?.categories.includes(AppCategories.calendar)) {
|
||||
try {
|
||||
const calendar = await getCalendar(credential);
|
||||
|
||||
const calendars = await calendar?.listCalendars();
|
||||
|
||||
const calendarIds = calendars?.map((cal) => cal.externalId);
|
||||
|
||||
await prisma.selectedCalendar.deleteMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
integration: credential.type as string,
|
||||
externalId: {
|
||||
in: calendarIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Error deleting selected calendars for userId: ${user.id} integration: ${credential.type}`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validated that credential is user's above
|
||||
await prisma.credential.delete({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
|
||||
// Backwards compatibility. Selected calendars cascade on delete when deleting a credential
|
||||
// If it's a calendar remove it from the SelectedCalendars
|
||||
if (credential.app?.categories.includes(AppCategories.calendar)) {
|
||||
const selectedCalendars = await prisma.selectedCalendar.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
integration: credential.type as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (selectedCalendars.length) {
|
||||
const calendar = await getCalendar(credential);
|
||||
|
||||
const calendars = await calendar?.listCalendars();
|
||||
|
||||
if (calendars && calendars.length > 0) {
|
||||
calendars.map(async (cal) => {
|
||||
prisma.selectedCalendar
|
||||
.delete({
|
||||
where: {
|
||||
userId_integration_externalId: {
|
||||
userId: user.id,
|
||||
externalId: cal.externalId,
|
||||
integration: cal.integration as string,
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") {
|
||||
console.log(
|
||||
`Error deleting selected calendars for user ${user.id} and calendar ${credential.appId}. Could not find selected calendar.`
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`Error deleting selected calendars for user ${user.id} and calendar ${credential.appId} with error: ${error}`
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,12 +7,7 @@ import { sendAdminOrganizationNotification } from "@calcom/emails";
|
|||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
||||
import {
|
||||
IS_TEAM_BILLING_ENABLED,
|
||||
RESERVED_SUBDOMAINS,
|
||||
IS_PRODUCTION,
|
||||
WEBAPP_URL,
|
||||
} from "@calcom/lib/constants";
|
||||
import { IS_TEAM_BILLING_ENABLED, RESERVED_SUBDOMAINS, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
@ -175,7 +170,6 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
|
|||
|
||||
return { user: { ...createOwnerOrg, password } };
|
||||
} else {
|
||||
if (!IS_PRODUCTION) return { checked: true };
|
||||
const language = await getTranslation(input.language ?? "en", "common");
|
||||
|
||||
const secret = createHash("md5")
|
||||
|
|
|
@ -2,6 +2,7 @@ import { createHash } from "crypto";
|
|||
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { totpRawCheck } from "@calcom/lib/totp";
|
||||
import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
@ -21,7 +22,10 @@ export const verifyCodeHandler = async ({ ctx, input }: VerifyCodeOptions) => {
|
|||
|
||||
if (!user || !email || !code) throw new TRPCError({ code: "BAD_REQUEST" });
|
||||
|
||||
if (!IS_PRODUCTION) return true;
|
||||
if (!IS_PRODUCTION || process.env.NEXT_PUBLIC_IS_E2E) {
|
||||
logger.warn(`Skipping code verification in dev/E2E environment`);
|
||||
return true;
|
||||
}
|
||||
await checkRateLimitAndThrowError({
|
||||
rateLimitingType: "core",
|
||||
identifier: email,
|
||||
|
|
|
@ -119,6 +119,11 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) =
|
|||
identifier: usernameOrEmail,
|
||||
token,
|
||||
expires: new Date(new Date().setHours(168)), // +1 week
|
||||
team: {
|
||||
connect: {
|
||||
id: team.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ type TestTriggerOptions = {
|
|||
};
|
||||
|
||||
export const testTriggerHandler = async ({ ctx: _ctx, input }: TestTriggerOptions) => {
|
||||
const { url, type, payloadTemplate = null } = input;
|
||||
const { url, type, payloadTemplate = null, secret = null } = input;
|
||||
const translation = await getTranslation("en", "common");
|
||||
const language = {
|
||||
locale: "en",
|
||||
|
@ -40,8 +40,8 @@ export const testTriggerHandler = async ({ ctx: _ctx, input }: TestTriggerOption
|
|||
};
|
||||
|
||||
try {
|
||||
const webhook = { subscriberUrl: url, payloadTemplate, appId: null, secret: null };
|
||||
return await sendPayload(null, type, new Date().toISOString(), webhook, data);
|
||||
const webhook = { subscriberUrl: url, appId: null, payloadTemplate };
|
||||
return await sendPayload(secret, type, new Date().toISOString(), webhook, data);
|
||||
} catch (_err) {
|
||||
const error = getErrorFromUnknown(_err);
|
||||
return {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { webhookIdAndEventTypeIdSchema } from "./types";
|
|||
|
||||
export const ZTestTriggerInputSchema = webhookIdAndEventTypeIdSchema.extend({
|
||||
url: z.string().url(),
|
||||
secret: z.string().optional(),
|
||||
type: z.string(),
|
||||
payloadTemplate: z.string().optional().nullable(),
|
||||
});
|
||||
|
|
|
@ -160,6 +160,7 @@ export interface CalendarEvent {
|
|||
team?: {
|
||||
name: string;
|
||||
members: TeamMember[];
|
||||
id: number;
|
||||
};
|
||||
location?: string | null;
|
||||
conferenceCredentialId?: number;
|
||||
|
|
|
@ -50,7 +50,10 @@ export function HintsOrErrors<T extends FieldValues = FieldValues>({
|
|||
return (
|
||||
<li
|
||||
key={key}
|
||||
className={error !== undefined ? (submitted ? "text-red-700" : "") : "text-green-600"}>
|
||||
data-testid="hint-error"
|
||||
className={
|
||||
error !== undefined ? (submitted ? "bg-yellow-200 text-red-700" : "") : "text-green-600"
|
||||
}>
|
||||
{error !== undefined ? (
|
||||
submitted ? (
|
||||
<X size="12" strokeWidth="3" className="-ml-1 inline-block ltr:mr-2 rtl:ml-2" />
|
||||
|
@ -72,7 +75,9 @@ export function HintsOrErrors<T extends FieldValues = FieldValues>({
|
|||
// errors exist, not custom ones, just show them as is
|
||||
if (fieldErrors) {
|
||||
return (
|
||||
<div className="text-gray mt-2 flex items-center gap-x-2 text-sm text-red-700">
|
||||
<div
|
||||
data-testid="field-error"
|
||||
className="text-gray mt-2 flex items-center gap-x-2 text-sm text-red-700">
|
||||
<div>
|
||||
<Info className="h-3 w-3" />
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,7 @@ export type SelectProps<
|
|||
Option,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
> = Props<Option, IsMulti, Group> & { variant?: "default" | "checkbox" };
|
||||
> = Props<Option, IsMulti, Group> & { variant?: "default" | "checkbox"; "data-testid"?: string };
|
||||
|
||||
export const Select = <
|
||||
Option,
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import type { GroupBase, InputProps, OptionProps } from "react-select";
|
||||
import type { GroupBase, InputProps, OptionProps, ControlProps } from "react-select";
|
||||
import { components as reactSelectComponents } from "react-select";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
import { UpgradeTeamsBadge, UpgradeOrgsBadge } from "../../badge";
|
||||
import { Check } from "../../icon";
|
||||
import type { SelectProps } from "./Select";
|
||||
|
||||
export const InputComponent = <
|
||||
Option,
|
||||
|
@ -60,6 +61,23 @@ export const OptionComponent = <
|
|||
);
|
||||
};
|
||||
|
||||
export const ControlComponent = <
|
||||
Option,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>(
|
||||
controlProps: ControlProps<Option, IsMulti, Group> & {
|
||||
selectProps: SelectProps<Option, IsMulti, Group>;
|
||||
}
|
||||
) => {
|
||||
const dataTestId = controlProps.selectProps["data-testid"] ?? "select-control";
|
||||
return (
|
||||
<span data-testid={dataTestId}>
|
||||
<reactSelectComponents.Control {...controlProps} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// We need to override this component if we need a icon - we can't simpily override styles
|
||||
type IconLeadingProps = {
|
||||
icon: React.ReactNode;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { GroupBase, SelectComponentsConfig, MenuPlacement } from "react-select";
|
||||
|
||||
import { InputComponent, OptionComponent } from "./components";
|
||||
import { InputComponent, OptionComponent, ControlComponent } from "./components";
|
||||
|
||||
export const getReactSelectProps = <
|
||||
Option,
|
||||
|
@ -18,6 +18,7 @@ export const getReactSelectProps = <
|
|||
components: {
|
||||
Input: InputComponent,
|
||||
Option: OptionComponent,
|
||||
Control: ControlComponent,
|
||||
...components,
|
||||
},
|
||||
unstyled: true,
|
||||
|
|
|
@ -209,6 +209,8 @@
|
|||
"CALCOM_CREDENTIAL_SYNC_ENDPOINT",
|
||||
"CALCOM_ENV",
|
||||
"CALCOM_LICENSE_KEY",
|
||||
"CALCOM_QA_EMAIL",
|
||||
"CALCOM_QA_PASSWORD",
|
||||
"CALCOM_TELEMETRY_DISABLED",
|
||||
"CALCOM_WEBHOOK_HEADER_NAME",
|
||||
"CALENDSO_ENCRYPTION_KEY",
|
||||
|
@ -222,6 +224,8 @@
|
|||
"DEBUG",
|
||||
"E2E_TEST_APPLE_CALENDAR_EMAIL",
|
||||
"E2E_TEST_APPLE_CALENDAR_PASSWORD",
|
||||
"E2E_TEST_CALCOM_QA_EMAIL",
|
||||
"E2E_TEST_CALCOM_QA_PASSWORD",
|
||||
"E2E_TEST_MAILHOG_ENABLED",
|
||||
"E2E_TEST_OIDC_CLIENT_ID",
|
||||
"E2E_TEST_OIDC_CLIENT_SECRET",
|
||||
|
|
380
yarn.lock
380
yarn.lock
|
@ -3484,7 +3484,7 @@ __metadata:
|
|||
lodash: ^4.17.21
|
||||
qs-stringify: ^1.2.1
|
||||
react-i18next: ^12.2.0
|
||||
stripe: ^14.3.0
|
||||
stripe: ^9.16.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
|
@ -3551,7 +3551,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
|
||||
|
@ -3629,7 +3629,7 @@ __metadata:
|
|||
prettier: ^2.8.6
|
||||
prettier-plugin-tailwindcss: ^0.2.5
|
||||
tailwind-scrollbar: ^2.0.1
|
||||
tailwindcss: ^3.3.1
|
||||
tailwindcss: ^3.3.3
|
||||
typescript: ^4.9.4
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
@ -3665,7 +3665,7 @@ __metadata:
|
|||
react-hook-form: ^7.43.3
|
||||
react-live-chat-loader: ^2.8.1
|
||||
swr: ^1.2.2
|
||||
tailwindcss: ^3.3.1
|
||||
tailwindcss: ^3.3.3
|
||||
typescript: ^4.9.4
|
||||
zod: ^3.22.2
|
||||
languageName: unknown
|
||||
|
@ -3796,7 +3796,7 @@ __metadata:
|
|||
autoprefixer: ^10.4.12
|
||||
npm-run-all: ^4.1.5
|
||||
postcss: ^8.4.18
|
||||
tailwindcss: ^3.3.1
|
||||
tailwindcss: ^3.3.3
|
||||
typescript: ^4.9.4
|
||||
vite: ^4.1.2
|
||||
languageName: unknown
|
||||
|
@ -4280,7 +4280,7 @@ __metadata:
|
|||
"@calcom/config": "*"
|
||||
"@calcom/dayjs": "*"
|
||||
"@calcom/ui": "*"
|
||||
"@radix-ui/react-avatar": ^1.0.0
|
||||
"@radix-ui/react-avatar": ^1.0.4
|
||||
"@radix-ui/react-collapsible": ^1.0.0
|
||||
"@radix-ui/react-dialog": ^1.0.4
|
||||
"@radix-ui/react-dropdown-menu": ^2.0.5
|
||||
|
@ -4316,7 +4316,7 @@ __metadata:
|
|||
storybook-addon-next-router: ^4.0.2
|
||||
storybook-addon-rtl-direction: ^0.0.19
|
||||
storybook-react-i18next: ^1.1.2
|
||||
tailwindcss: ^3.3.1
|
||||
tailwindcss: ^3.3.3
|
||||
typescript: ^4.9.4
|
||||
vite: ^4.1.2
|
||||
languageName: unknown
|
||||
|
@ -4522,7 +4522,7 @@ __metadata:
|
|||
"@next-auth/prisma-adapter": ^1.0.4
|
||||
"@next/bundle-analyzer": ^13.1.6
|
||||
"@playwright/test": ^1.31.2
|
||||
"@radix-ui/react-avatar": ^1.0.0
|
||||
"@radix-ui/react-avatar": ^1.0.4
|
||||
"@radix-ui/react-collapsible": ^1.0.0
|
||||
"@radix-ui/react-dialog": ^1.0.4
|
||||
"@radix-ui/react-dropdown-menu": ^2.0.5
|
||||
|
@ -4635,7 +4635,7 @@ __metadata:
|
|||
short-uuid: ^4.2.0
|
||||
stripe: ^9.16.0
|
||||
superjson: 1.9.1
|
||||
tailwindcss: ^3.3.1
|
||||
tailwindcss: ^3.3.3
|
||||
tailwindcss-animate: ^1.0.6
|
||||
tailwindcss-radix: ^2.6.0
|
||||
ts-node: ^10.9.1
|
||||
|
@ -7824,13 +7824,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/env@npm:13.4.6":
|
||||
version: 13.4.6
|
||||
resolution: "@next/env@npm:13.4.6"
|
||||
checksum: 65d6cfb68adf5067f5e42f339e46908aca5a14fbc78f1e42e0becec1617da108cf68621c98f5a2c2e18da5a7955e355e98d5c4a7894222401bb374b2ca1c08f4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/env@npm:13.5.5":
|
||||
version: 13.5.5
|
||||
resolution: "@next/env@npm:13.5.5"
|
||||
|
@ -7838,6 +7831,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/env@npm:13.5.6":
|
||||
version: 13.5.6
|
||||
resolution: "@next/env@npm:13.5.6"
|
||||
checksum: 5e8f3f6f987a15dad3cd7b2bcac64a6382c2ec372d95d0ce6ab295eb59c9731222017eebf71ff3005932de2571f7543bce7e5c6a8c90030207fb819404138dc2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/eslint-plugin-next@npm:13.2.1":
|
||||
version: 13.2.1
|
||||
resolution: "@next/eslint-plugin-next@npm:13.2.1"
|
||||
|
@ -7847,13 +7847,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-darwin-arm64@npm:13.4.6":
|
||||
version: 13.4.6
|
||||
resolution: "@next/swc-darwin-arm64@npm:13.4.6"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-darwin-arm64@npm:13.5.5":
|
||||
version: 13.5.5
|
||||
resolution: "@next/swc-darwin-arm64@npm:13.5.5"
|
||||
|
@ -7861,10 +7854,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-darwin-x64@npm:13.4.6":
|
||||
version: 13.4.6
|
||||
resolution: "@next/swc-darwin-x64@npm:13.4.6"
|
||||
conditions: os=darwin & cpu=x64
|
||||
"@next/swc-darwin-arm64@npm:13.5.6":
|
||||
version: 13.5.6
|
||||
resolution: "@next/swc-darwin-arm64@npm:13.5.6"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -7875,10 +7868,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-linux-arm64-gnu@npm:13.4.6":
|
||||
version: 13.4.6
|
||||
resolution: "@next/swc-linux-arm64-gnu@npm:13.4.6"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
"@next/swc-darwin-x64@npm:13.5.6":
|
||||
version: 13.5.6
|
||||
resolution: "@next/swc-darwin-x64@npm:13.5.6"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -7889,10 +7882,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-linux-arm64-musl@npm:13.4.6":
|
||||
version: 13.4.6
|
||||
resolution: "@next/swc-linux-arm64-musl@npm:13.4.6"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
"@next/swc-linux-arm64-gnu@npm:13.5.6":
|
||||
version: 13.5.6
|
||||
resolution: "@next/swc-linux-arm64-gnu@npm:13.5.6"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -7903,10 +7896,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-linux-x64-gnu@npm:13.4.6":
|
||||
version: 13.4.6
|
||||
resolution: "@next/swc-linux-x64-gnu@npm:13.4.6"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
"@next/swc-linux-arm64-musl@npm:13.5.6":
|
||||
version: 13.5.6
|
||||
resolution: "@next/swc-linux-arm64-musl@npm:13.5.6"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -7917,10 +7910,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-linux-x64-musl@npm:13.4.6":
|
||||
version: 13.4.6
|
||||
resolution: "@next/swc-linux-x64-musl@npm:13.4.6"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
"@next/swc-linux-x64-gnu@npm:13.5.6":
|
||||
version: 13.5.6
|
||||
resolution: "@next/swc-linux-x64-gnu@npm:13.5.6"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -7931,10 +7924,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-win32-arm64-msvc@npm:13.4.6":
|
||||
version: 13.4.6
|
||||
resolution: "@next/swc-win32-arm64-msvc@npm:13.4.6"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
"@next/swc-linux-x64-musl@npm:13.5.6":
|
||||
version: 13.5.6
|
||||
resolution: "@next/swc-linux-x64-musl@npm:13.5.6"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -7945,10 +7938,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-win32-ia32-msvc@npm:13.4.6":
|
||||
version: 13.4.6
|
||||
resolution: "@next/swc-win32-ia32-msvc@npm:13.4.6"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
"@next/swc-win32-arm64-msvc@npm:13.5.6":
|
||||
version: 13.5.6
|
||||
resolution: "@next/swc-win32-arm64-msvc@npm:13.5.6"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -7959,10 +7952,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-win32-x64-msvc@npm:13.4.6":
|
||||
version: 13.4.6
|
||||
resolution: "@next/swc-win32-x64-msvc@npm:13.4.6"
|
||||
conditions: os=win32 & cpu=x64
|
||||
"@next/swc-win32-ia32-msvc@npm:13.5.6":
|
||||
version: 13.5.6
|
||||
resolution: "@next/swc-win32-ia32-msvc@npm:13.5.6"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -7973,6 +7966,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-win32-x64-msvc@npm:13.5.6":
|
||||
version: 13.5.6
|
||||
resolution: "@next/swc-win32-x64-msvc@npm:13.5.6"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@noble/curves@npm:1.1.0, @noble/curves@npm:~1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "@noble/curves@npm:1.1.0"
|
||||
|
@ -8807,29 +8807,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-avatar@npm:^1.0.0":
|
||||
version: 1.0.3
|
||||
resolution: "@radix-ui/react-avatar@npm:1.0.3"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/react-context": 1.0.1
|
||||
"@radix-ui/react-primitive": 1.0.3
|
||||
"@radix-ui/react-use-callback-ref": 1.0.1
|
||||
"@radix-ui/react-use-layout-effect": 1.0.1
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: defd6070ca89d64f46017943bc444ac73ca4711f710b8fcb7152f5fb84cb05f39e7944d372695720929fa666cf9bc65ce95725c5a29d81f9e51aba59aed42ca2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-avatar@npm:^1.0.4":
|
||||
version: 1.0.4
|
||||
resolution: "@radix-ui/react-avatar@npm:1.0.4"
|
||||
|
@ -12448,15 +12425,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/helpers@npm:0.5.1":
|
||||
version: 0.5.1
|
||||
resolution: "@swc/helpers@npm:0.5.1"
|
||||
dependencies:
|
||||
tslib: ^2.4.0
|
||||
checksum: 71e0e27234590435e4c62b97ef5e796f88e786841a38c7116a5e27a3eafa7b9ead7cdec5249b32165902076de78446945311c973e59bddf77c1e24f33a8f272a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/helpers@npm:0.5.2":
|
||||
version: 0.5.2
|
||||
resolution: "@swc/helpers@npm:0.5.2"
|
||||
|
@ -13331,6 +13299,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/jsdom@npm:^21.1.3":
|
||||
version: 21.1.4
|
||||
resolution: "@types/jsdom@npm:21.1.4"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
"@types/tough-cookie": "*"
|
||||
parse5: ^7.0.0
|
||||
checksum: 915f619111dadd8d1bb7f12b6736c9d2e486911e1aed086de5fb003e7e40ae1e368da322dc04f2122ef47faf40ca75b9315ae2df3e8011f882dcf84660fb0d68
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/jsforce@npm:^1.11.0":
|
||||
version: 1.11.0
|
||||
resolution: "@types/jsforce@npm:1.11.0"
|
||||
|
@ -13848,6 +13827,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/tough-cookie@npm:*":
|
||||
version: 4.0.5
|
||||
resolution: "@types/tough-cookie@npm:4.0.5"
|
||||
checksum: f19409d0190b179331586365912920d192733112a195e870c7f18d20ac8adb7ad0b0ff69dad430dba8bc2be09593453a719cfea92dc3bda19748fd158fe1498d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/trusted-types@npm:^2.0.2":
|
||||
version: 2.0.2
|
||||
resolution: "@types/trusted-types@npm:2.0.2"
|
||||
|
@ -17197,6 +17183,7 @@ __metadata:
|
|||
"@playwright/test": ^1.31.2
|
||||
"@snaplet/copycat": ^0.3.0
|
||||
"@testing-library/jest-dom": ^5.16.5
|
||||
"@types/jsdom": ^21.1.3
|
||||
"@types/jsonwebtoken": ^9.0.3
|
||||
c8: ^7.13.0
|
||||
checkly: latest
|
||||
|
@ -18178,7 +18165,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"color-name@npm:^1.1.4, color-name@npm:~1.1.4":
|
||||
"color-name@npm:~1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "color-name@npm:1.1.4"
|
||||
checksum: b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610
|
||||
|
@ -26356,15 +26343,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jiti@npm:^1.17.2":
|
||||
version: 1.18.2
|
||||
resolution: "jiti@npm:1.18.2"
|
||||
bin:
|
||||
jiti: bin/jiti.js
|
||||
checksum: 46c41cd82d01c6efdee3fc0ae9b3e86ed37457192d6366f19157d863d64961b07982ab04e9d5879576a1af99cc4d132b0b73b336094f86a5ce9fb1029ec2d29f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"joi@npm:^17.7.0":
|
||||
version: 17.10.2
|
||||
resolution: "joi@npm:17.10.2"
|
||||
|
@ -27550,13 +27528,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lilconfig@npm:^2.0.6":
|
||||
version: 2.0.6
|
||||
resolution: "lilconfig@npm:2.0.6"
|
||||
checksum: 40a3cd72f103b1be5975f2ac1850810b61d4053e20ab09be8d3aeddfe042187e1ba70b4651a7e70f95efa1642e7dc8b2ae395b317b7d7753b241b43cef7c0f7d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lilconfig@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "lilconfig@npm:2.1.0"
|
||||
|
@ -30247,29 +30218,27 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"next@npm:^13.4.6":
|
||||
version: 13.4.6
|
||||
resolution: "next@npm:13.4.6"
|
||||
version: 13.5.6
|
||||
resolution: "next@npm:13.5.6"
|
||||
dependencies:
|
||||
"@next/env": 13.4.6
|
||||
"@next/swc-darwin-arm64": 13.4.6
|
||||
"@next/swc-darwin-x64": 13.4.6
|
||||
"@next/swc-linux-arm64-gnu": 13.4.6
|
||||
"@next/swc-linux-arm64-musl": 13.4.6
|
||||
"@next/swc-linux-x64-gnu": 13.4.6
|
||||
"@next/swc-linux-x64-musl": 13.4.6
|
||||
"@next/swc-win32-arm64-msvc": 13.4.6
|
||||
"@next/swc-win32-ia32-msvc": 13.4.6
|
||||
"@next/swc-win32-x64-msvc": 13.4.6
|
||||
"@swc/helpers": 0.5.1
|
||||
"@next/env": 13.5.6
|
||||
"@next/swc-darwin-arm64": 13.5.6
|
||||
"@next/swc-darwin-x64": 13.5.6
|
||||
"@next/swc-linux-arm64-gnu": 13.5.6
|
||||
"@next/swc-linux-arm64-musl": 13.5.6
|
||||
"@next/swc-linux-x64-gnu": 13.5.6
|
||||
"@next/swc-linux-x64-musl": 13.5.6
|
||||
"@next/swc-win32-arm64-msvc": 13.5.6
|
||||
"@next/swc-win32-ia32-msvc": 13.5.6
|
||||
"@next/swc-win32-x64-msvc": 13.5.6
|
||||
"@swc/helpers": 0.5.2
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: ^1.0.30001406
|
||||
postcss: 8.4.14
|
||||
postcss: 8.4.31
|
||||
styled-jsx: 5.1.1
|
||||
watchpack: 2.4.0
|
||||
zod: 3.21.4
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ^1.1.0
|
||||
fibers: ">= 3.1.0"
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
sass: ^1.3.0
|
||||
|
@ -30295,13 +30264,11 @@ __metadata:
|
|||
peerDependenciesMeta:
|
||||
"@opentelemetry/api":
|
||||
optional: true
|
||||
fibers:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
bin:
|
||||
next: dist/bin/next
|
||||
checksum: 1d28d4be184b1311c42f01ce12d3636e3439332aebcf211b0b554164966f053a609db529d7194824b68537256625767c5bc9f7655a9d42af72b8c7ce4c0d4104
|
||||
checksum: c869b0014ae921ada3bf22301985027ec320aebcd6aa9c16e8afbded68bb8def5874cca034c680e8c351a79578f1e514971d02777f6f0a5a1d7290f25970ac0d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -31761,7 +31728,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parse5@npm:^7.1.2":
|
||||
"parse5@npm:^7.0.0, parse5@npm:^7.1.2":
|
||||
version: 7.1.2
|
||||
resolution: "parse5@npm:7.1.2"
|
||||
dependencies:
|
||||
|
@ -32362,19 +32329,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-import@npm:^14.1.0":
|
||||
version: 14.1.0
|
||||
resolution: "postcss-import@npm:14.1.0"
|
||||
dependencies:
|
||||
postcss-value-parser: ^4.0.0
|
||||
read-cache: ^1.0.0
|
||||
resolve: ^1.1.7
|
||||
peerDependencies:
|
||||
postcss: ^8.0.0
|
||||
checksum: cd45d406e90f67cdab9524352e573cc6b4462b790934a05954e929a6653ebd31288ceebc8ce3c3ed7117ae672d9ebbec57df0bceec0a56e9b259c2e71d47ca86
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-import@npm:^15.1.0":
|
||||
version: 15.1.0
|
||||
resolution: "postcss-import@npm:15.1.0"
|
||||
|
@ -32388,17 +32342,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-js@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "postcss-js@npm:4.0.0"
|
||||
dependencies:
|
||||
camelcase-css: ^2.0.1
|
||||
peerDependencies:
|
||||
postcss: ^8.3.3
|
||||
checksum: 14be8a58670b4c5d037d40f179240a4f736d53530db727e2635638fa296bc4bff18149ca860928398aace422e55d07c9f5729eeccd395340944985199cdc82a5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-js@npm:^4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "postcss-js@npm:4.0.1"
|
||||
|
@ -32410,24 +32353,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-load-config@npm:^3.1.4":
|
||||
version: 3.1.4
|
||||
resolution: "postcss-load-config@npm:3.1.4"
|
||||
dependencies:
|
||||
lilconfig: ^2.0.5
|
||||
yaml: ^1.10.2
|
||||
peerDependencies:
|
||||
postcss: ">=8.0.9"
|
||||
ts-node: ">=9.0.0"
|
||||
peerDependenciesMeta:
|
||||
postcss:
|
||||
optional: true
|
||||
ts-node:
|
||||
optional: true
|
||||
checksum: 1c589504c2d90b1568aecae8238ab993c17dba2c44f848a8f13619ba556d26a1c09644d5e6361b5784e721e94af37b604992f9f3dc0483e687a0cc1cc5029a34
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-load-config@npm:^4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "postcss-load-config@npm:4.0.1"
|
||||
|
@ -32561,17 +32486,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-nested@npm:6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "postcss-nested@npm:6.0.0"
|
||||
dependencies:
|
||||
postcss-selector-parser: ^6.0.10
|
||||
peerDependencies:
|
||||
postcss: ^8.2.14
|
||||
checksum: 2105dc52cd19747058f1a46862c9e454b5a365ac2e7135fc1015d67a8fe98ada2a8d9ee578e90f7a093bd55d3994dd913ba5ff1d5e945b4ed9a8a2992ecc8f10
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-nested@npm:^6.0.1":
|
||||
version: 6.0.1
|
||||
resolution: "postcss-nested@npm:6.0.1"
|
||||
|
@ -32592,7 +32506,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-selector-parser@npm:^6.0.0, postcss-selector-parser@npm:^6.0.10, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4":
|
||||
"postcss-selector-parser@npm:^6.0.0, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4":
|
||||
version: 6.0.10
|
||||
resolution: "postcss-selector-parser@npm:6.0.10"
|
||||
dependencies:
|
||||
|
@ -32626,17 +32540,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss@npm:8.4.14, postcss@npm:^8.2.14":
|
||||
version: 8.4.14
|
||||
resolution: "postcss@npm:8.4.14"
|
||||
dependencies:
|
||||
nanoid: ^3.3.4
|
||||
picocolors: ^1.0.0
|
||||
source-map-js: ^1.0.2
|
||||
checksum: fe58766ff32e4becf65a7d57678995cfd239df6deed2fe0557f038b47c94e4132e7e5f68b5aa820c13adfec32e523b693efaeb65798efb995ce49ccd83953816
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss@npm:8.4.31":
|
||||
version: 8.4.31
|
||||
resolution: "postcss@npm:8.4.31"
|
||||
|
@ -32658,14 +32561,14 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss@npm:^8.0.9, postcss@npm:^8.3.11":
|
||||
version: 8.4.21
|
||||
resolution: "postcss@npm:8.4.21"
|
||||
"postcss@npm:^8.2.14":
|
||||
version: 8.4.14
|
||||
resolution: "postcss@npm:8.4.14"
|
||||
dependencies:
|
||||
nanoid: ^3.3.4
|
||||
picocolors: ^1.0.0
|
||||
source-map-js: ^1.0.2
|
||||
checksum: e39ac60ccd1542d4f9d93d894048aac0d686b3bb38e927d8386005718e6793dbbb46930f0a523fe382f1bbd843c6d980aaea791252bf5e176180e5a4336d9679
|
||||
checksum: fe58766ff32e4becf65a7d57678995cfd239df6deed2fe0557f038b47c94e4132e7e5f68b5aa820c13adfec32e523b693efaeb65798efb995ce49ccd83953816
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -32680,6 +32583,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss@npm:^8.3.11":
|
||||
version: 8.4.21
|
||||
resolution: "postcss@npm:8.4.21"
|
||||
dependencies:
|
||||
nanoid: ^3.3.4
|
||||
picocolors: ^1.0.0
|
||||
source-map-js: ^1.0.2
|
||||
checksum: e39ac60ccd1542d4f9d93d894048aac0d686b3bb38e927d8386005718e6793dbbb46930f0a523fe382f1bbd843c6d980aaea791252bf5e176180e5a4336d9679
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss@npm:^8.4.23":
|
||||
version: 8.4.23
|
||||
resolution: "postcss@npm:8.4.23"
|
||||
|
@ -33351,15 +33265,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"qs@npm:^6.11.0":
|
||||
version: 6.11.2
|
||||
resolution: "qs@npm:6.11.2"
|
||||
dependencies:
|
||||
side-channel: ^1.0.4
|
||||
checksum: e812f3c590b2262548647d62f1637b6989cc56656dc960b893fe2098d96e1bd633f36576f4cd7564dfbff9db42e17775884db96d846bebe4f37420d073ecdc0b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"qs@npm:~6.5.2":
|
||||
version: 6.5.3
|
||||
resolution: "qs@npm:6.5.3"
|
||||
|
@ -37411,16 +37316,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stripe@npm:^14.3.0":
|
||||
version: 14.3.0
|
||||
resolution: "stripe@npm:14.3.0"
|
||||
dependencies:
|
||||
"@types/node": ">=8.1.0"
|
||||
qs: ^6.11.0
|
||||
checksum: 1aa0dec1fe8cd4c0d2a5378b9d3c69f7df505efdc86b8d6352e194d656129db83b9faaf189b5138fb5fd9a0b90e618dfcff854bb4773d289a0de0b65d0a94cb2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stripe@npm:^9.16.0":
|
||||
version: 9.16.0
|
||||
resolution: "stripe@npm:9.16.0"
|
||||
|
@ -37501,23 +37396,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sucrase@npm:^3.29.0":
|
||||
version: 3.31.0
|
||||
resolution: "sucrase@npm:3.31.0"
|
||||
dependencies:
|
||||
commander: ^4.0.0
|
||||
glob: 7.1.6
|
||||
lines-and-columns: ^1.1.6
|
||||
mz: ^2.7.0
|
||||
pirates: ^4.0.1
|
||||
ts-interface-checker: ^0.1.9
|
||||
bin:
|
||||
sucrase: bin/sucrase
|
||||
sucrase-node: bin/sucrase-node
|
||||
checksum: 333990b1bca57acc010ae07c763dddfd34f01fd38afe9e53cf43f4a5096bd7a66f924fed65770288fba475f914f3aa5277cc4490ed9e74c50b4cea7f147e9e63
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sucrase@npm:^3.32.0":
|
||||
version: 3.34.0
|
||||
resolution: "sucrase@npm:3.34.0"
|
||||
|
@ -37897,43 +37775,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tailwindcss@npm:^3.3.1":
|
||||
version: 3.3.1
|
||||
resolution: "tailwindcss@npm:3.3.1"
|
||||
dependencies:
|
||||
arg: ^5.0.2
|
||||
chokidar: ^3.5.3
|
||||
color-name: ^1.1.4
|
||||
didyoumean: ^1.2.2
|
||||
dlv: ^1.1.3
|
||||
fast-glob: ^3.2.12
|
||||
glob-parent: ^6.0.2
|
||||
is-glob: ^4.0.3
|
||||
jiti: ^1.17.2
|
||||
lilconfig: ^2.0.6
|
||||
micromatch: ^4.0.5
|
||||
normalize-path: ^3.0.0
|
||||
object-hash: ^3.0.0
|
||||
picocolors: ^1.0.0
|
||||
postcss: ^8.0.9
|
||||
postcss-import: ^14.1.0
|
||||
postcss-js: ^4.0.0
|
||||
postcss-load-config: ^3.1.4
|
||||
postcss-nested: 6.0.0
|
||||
postcss-selector-parser: ^6.0.11
|
||||
postcss-value-parser: ^4.2.0
|
||||
quick-lru: ^5.1.1
|
||||
resolve: ^1.22.1
|
||||
sucrase: ^3.29.0
|
||||
peerDependencies:
|
||||
postcss: ^8.0.9
|
||||
bin:
|
||||
tailwind: lib/cli.js
|
||||
tailwindcss: lib/cli.js
|
||||
checksum: 966ba175486fb65ef3dd76aa8ec6929ff1d168531843ca7d5faf680b7097c36bf5f9ca385b563cdfdff935bb2bd37ac5998e877491407867503cc129d118bf93
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tailwindcss@npm:^3.3.3":
|
||||
version: 3.3.3
|
||||
resolution: "tailwindcss@npm:3.3.3"
|
||||
|
@ -42114,13 +41955,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zod@npm:3.21.4":
|
||||
version: 3.21.4
|
||||
resolution: "zod@npm:3.21.4"
|
||||
checksum: f185ba87342ff16f7a06686767c2b2a7af41110c7edf7c1974095d8db7a73792696bcb4a00853de0d2edeb34a5b2ea6a55871bc864227dace682a0a28de33e1f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zod@npm:^3.21.4, zod@npm:^3.22.2":
|
||||
version: 3.22.2
|
||||
resolution: "zod@npm:3.22.2"
|
||||
|
|
Loading…
Reference in New Issue
Block a user