Merge branch 'main' into platform
This commit is contained in:
commit
104f6eb38a
|
@ -37,6 +37,7 @@ BASECAMP3_USER_AGENT=
|
|||
|
||||
DAILY_API_KEY=
|
||||
DAILY_SCALE_PLAN=''
|
||||
DAILY_WEBHOOK_SECRET=''
|
||||
|
||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||
# Needed to enable Google Calendar integration and Login with Google
|
||||
|
|
13
.env.example
13
.env.example
|
@ -196,6 +196,10 @@ EMAIL_SERVER_PORT=1025
|
|||
# Make sure to run mailhog container manually or with `yarn dx`
|
||||
E2E_TEST_MAILHOG_ENABLED=
|
||||
|
||||
# Resend
|
||||
# Send transactional email using resend
|
||||
# RESEND_API_KEY=
|
||||
|
||||
# **********************************************************************************************************
|
||||
|
||||
# Set the following value to true if you wish to enable Team Impersonation
|
||||
|
@ -291,5 +295,14 @@ AB_TEST_BUCKET_PROBABILITY=50
|
|||
# whether we redirect to the future/event-types from event-types or not
|
||||
APP_ROUTER_EVENT_TYPES_ENABLED=1
|
||||
APP_ROUTER_SETTINGS_ADMIN_ENABLED=1
|
||||
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=1
|
||||
APP_ROUTER_APPS_SLUG_ENABLED=1
|
||||
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=1
|
||||
# whether we redirect to the future/apps/categories from /apps/categories or not
|
||||
APP_ROUTER_APPS_CATEGORIES_ENABLED=1
|
||||
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
|
||||
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=1
|
||||
|
||||
|
||||
# api v2
|
||||
NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
|
|
@ -23,6 +23,7 @@ Fixes # (issue)
|
|||
- [ ] Chore (refactoring code, technical debt, workflow improvements)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Tests (Unit/Integration/E2E or any other test)
|
||||
- [ ] This change requires a documentation update
|
||||
|
||||
## How should this be tested?
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
diff --git a/index.cjs b/index.cjs
|
||||
index b645707a3549fc298508726e404243499bbed499..f34b0891e99b275a9218e253f303f43d31ef3f73 100644
|
||||
index c83f700ae9998cd87b4c2d66ecbb2ad3d7b4603c..76a2200b57f0b9243e2c61464d578b67746ad5a4 100644
|
||||
--- a/index.cjs
|
||||
+++ b/index.cjs
|
||||
@@ -13,8 +13,8 @@ function withMetadataArgument(func, _arguments) {
|
|
@ -221,7 +221,6 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
|||
|
||||
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
|
||||
|
||||
1. Set a 24 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
|
||||
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
||||
|
||||
In a development environment, run:
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export {};
|
|
@ -0,0 +1,5 @@
|
|||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
|
@ -1,6 +1,8 @@
|
|||
const { withAxiom } = require("next-axiom");
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
|
||||
module.exports = withAxiom({
|
||||
const plugins = [withAxiom];
|
||||
const nextConfig = {
|
||||
transpilePackages: [
|
||||
"@calcom/app-store",
|
||||
"@calcom/core",
|
||||
|
@ -66,4 +68,15 @@ module.exports = withAxiom({
|
|||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
nextConfig["sentry"] = {
|
||||
autoInstrumentServerFunctions: true,
|
||||
hideSourceMaps: true,
|
||||
};
|
||||
|
||||
plugins.push(withSentryConfig);
|
||||
}
|
||||
|
||||
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||
|
|
|
@ -6,19 +6,45 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
|||
|
||||
async function authMiddleware(req: NextApiRequest) {
|
||||
const { userId, prisma, isAdmin, query } = req;
|
||||
if (isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const userWithBookings = await prisma.user.findUnique({
|
||||
const userWithBookingsAndTeamIds = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: { bookings: true },
|
||||
include: {
|
||||
bookings: true,
|
||||
teams: {
|
||||
select: {
|
||||
teamId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!userWithBookings) throw new HttpError({ statusCode: 404, message: "User not found" });
|
||||
if (!userWithBookingsAndTeamIds) throw new HttpError({ statusCode: 404, message: "User not found" });
|
||||
|
||||
const userBookingIds = userWithBookings.bookings.map((booking) => booking.id);
|
||||
const userBookingIds = userWithBookingsAndTeamIds.bookings.map((booking) => booking.id);
|
||||
|
||||
if (!isAdmin && !userBookingIds.includes(id)) {
|
||||
if (!userBookingIds.includes(id)) {
|
||||
const teamBookings = await prisma.booking.findUnique({
|
||||
where: {
|
||||
id: id,
|
||||
eventType: {
|
||||
team: {
|
||||
id: {
|
||||
in: userWithBookingsAndTeamIds.teams.map((team) => team.teamId),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!teamBookings) {
|
||||
throw new HttpError({ statusCode: 401, message: "You are not authorized" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { isSupportedTimeZone } from "@calcom/lib/date-fns";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
|
@ -9,10 +13,34 @@ import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util
|
|||
import { TRPCError } from "@trpc/server";
|
||||
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
||||
|
||||
// Apply plugins
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const input = getScheduleSchema.parse(req.query);
|
||||
return await getAvailableSlots({ ctx: await createContext({ req, res }), input });
|
||||
const { usernameList, ...rest } = req.query;
|
||||
let slugs = usernameList;
|
||||
if (!Array.isArray(usernameList)) {
|
||||
slugs = usernameList ? [usernameList] : [];
|
||||
}
|
||||
const input = getScheduleSchema.parse({ usernameList: slugs, ...rest });
|
||||
const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false;
|
||||
const availableSlots = await getAvailableSlots({ ctx: await createContext({ req, res }), input });
|
||||
const slotsInProvidedTimeZone = timeZoneSupported
|
||||
? Object.keys(availableSlots.slots).reduce(
|
||||
(acc: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>, date) => {
|
||||
acc[date] = availableSlots.slots[date].map((slot) => ({
|
||||
...slot,
|
||||
time: dayjs(slot.time).tz(input.timeZone).format(),
|
||||
}));
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
)
|
||||
: availableSlots;
|
||||
|
||||
return slotsInProvidedTimeZone;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (cause) {
|
||||
if (cause instanceof TRPCError) {
|
||||
|
|
|
@ -8,7 +8,9 @@ pnpm-debug.log*
|
|||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
storybook-static
|
||||
storybook-static/*
|
||||
!storybook-static/favicon.ico
|
||||
!storybook-static/sb-cover.jpg
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import { dirname, join } from "path";
|
||||
import type { StorybookConfig } from "@storybook/nextjs";
|
||||
import path, { dirname, join } from "path";
|
||||
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
"../intro.stories.mdx",
|
||||
"../../../packages/ui/components/**/*.stories.mdx",
|
||||
"../../../packages/platform/atoms/**/*.stories.mdx",
|
||||
"../../../packages/features/**/*.stories.mdx",
|
||||
"../../../packages/ui/components/**/*.stories.mdx", // legacy SB6 stories
|
||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../../../packages/ui/components/**/*.docs.mdx",
|
||||
"../../../packages/features/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../../../packages/features/**/*.docs.mdx",
|
||||
"../../../packages/atoms/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../../../packages/atoms/**/*.docs.mdx",
|
||||
],
|
||||
|
||||
addons: [
|
||||
|
@ -17,23 +19,23 @@ module.exports = {
|
|||
getAbsolutePath("@storybook/addon-interactions"),
|
||||
getAbsolutePath("storybook-addon-rtl-direction"),
|
||||
getAbsolutePath("storybook-react-i18next"),
|
||||
getAbsolutePath("@storybook/addon-mdx-gfm"),
|
||||
],
|
||||
|
||||
framework: {
|
||||
name: getAbsolutePath("@storybook/nextjs"),
|
||||
name: getAbsolutePath("@storybook/nextjs") as "@storybook/nextjs",
|
||||
|
||||
options: {
|
||||
builder: {
|
||||
fsCache: true,
|
||||
lazyCompilation: true,
|
||||
},
|
||||
// builder: {
|
||||
// fsCache: true,
|
||||
// lazyCompilation: true,
|
||||
// },
|
||||
},
|
||||
},
|
||||
|
||||
staticDirs: ["../public"],
|
||||
|
||||
webpackFinal: async (config, { configType }) => {
|
||||
config.resolve = config.resolve || {};
|
||||
config.resolve.fallback = {
|
||||
fs: false,
|
||||
assert: false,
|
||||
|
@ -61,6 +63,8 @@ module.exports = {
|
|||
zlib: false,
|
||||
};
|
||||
|
||||
config.module = config.module || {};
|
||||
config.module.rules = config.module.rules || [];
|
||||
config.module.rules.push({
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
|
@ -85,6 +89,8 @@ module.exports = {
|
|||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
function getAbsolutePath(value) {
|
||||
return dirname(require.resolve(join(value, "package.json")));
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
import "../styles/globals.css";
|
||||
import "../styles/storybook-styles.css";
|
||||
import i18n from "./i18next";
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
|
||||
globals: {
|
||||
locale: "en",
|
||||
locales: {
|
||||
en: "English",
|
||||
fr: "Français",
|
||||
},
|
||||
},
|
||||
i18n,
|
||||
};
|
||||
|
||||
const withI18next = (Story) => (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<div style={{ margin: "2rem" }}>
|
||||
<Story />
|
||||
</div>
|
||||
</I18nextProvider>
|
||||
);
|
||||
|
||||
export const decorators = [withI18next];
|
||||
|
||||
window.getEmbedNamespace = () => {
|
||||
const url = new URL(document.URL);
|
||||
const namespace = url.searchParams.get("embed");
|
||||
return namespace;
|
||||
};
|
||||
|
||||
window.getEmbedTheme = () => {
|
||||
return "auto";
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
// adds tooltip context to all stories
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import type { Preview } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
import type { EmbedThemeConfig } from "@calcom/embed-core/src/types";
|
||||
// adds trpc context to all stories (esp. booker)
|
||||
import { StorybookTrpcProvider } from "@calcom/ui";
|
||||
|
||||
import "../styles/globals.css";
|
||||
import "../styles/storybook-styles.css";
|
||||
import i18n from "./i18next";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
|
||||
globals: {
|
||||
locale: "en",
|
||||
locales: {
|
||||
en: "English",
|
||||
fr: "Français",
|
||||
},
|
||||
},
|
||||
|
||||
i18n,
|
||||
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<StorybookTrpcProvider>
|
||||
<TooltipProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<div style={{ margin: "2rem" }}>
|
||||
<Story />
|
||||
</div>
|
||||
</I18nextProvider>
|
||||
</TooltipProvider>
|
||||
</StorybookTrpcProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default preview;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
getEmbedNamespace: () => string | null;
|
||||
getEmbedTheme: () => EmbedThemeConfig | null;
|
||||
}
|
||||
}
|
||||
|
||||
window.getEmbedNamespace = () => {
|
||||
const url = new URL(document.URL);
|
||||
const namespace = url.searchParams.get("embed");
|
||||
return namespace;
|
||||
};
|
||||
|
||||
window.getEmbedTheme = () => {
|
||||
return "auto";
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { ArgsTable } from "@storybook/addon-docs";
|
||||
import { SortType } from "@storybook/components";
|
||||
import { PropDescriptor } from "@storybook/store";
|
||||
import type { SortType } from "@storybook/blocks";
|
||||
import type { PropDescriptor } from "@storybook/preview-api";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore storybook addon types component as any so we have to do
|
||||
type Component = any;
|
||||
|
|
|
@ -20,7 +20,10 @@
|
|||
"@radix-ui/react-slider": "^1.0.0",
|
||||
"@radix-ui/react-switch": "^1.0.0",
|
||||
"@radix-ui/react-tooltip": "^1.0.0",
|
||||
"@storybook/addon-docs": "^7.6.3",
|
||||
"@storybook/blocks": "^7.6.3",
|
||||
"@storybook/nextjs": "^7.6.3",
|
||||
"@storybook/preview-api": "^7.6.3",
|
||||
"next": "^13.4.6",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -33,7 +36,6 @@
|
|||
"@storybook/addon-essentials": "^7.6.3",
|
||||
"@storybook/addon-interactions": "^7.6.3",
|
||||
"@storybook/addon-links": "^7.6.3",
|
||||
"@storybook/addon-mdx-gfm": "^7.6.3",
|
||||
"@storybook/nextjs": "^7.6.3",
|
||||
"@storybook/react": "^7.6.3",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
|
|
|
@ -6,6 +6,11 @@ import z from "zod";
|
|||
const ROUTES: [URLPattern, boolean][] = [
|
||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
||||
["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const,
|
||||
["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const,
|
||||
["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const,
|
||||
["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const,
|
||||
["/apps/categories", process.env.APP_ROUTER_APPS_CATEGORIES_ENABLED === "1"] as const,
|
||||
["/apps/categories/:category", process.env.APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED === "1"] as const,
|
||||
].map(([pathname, enabled]) => [
|
||||
new URLPattern({
|
||||
pathname,
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
// originally from in the "experimental playground for tRPC + next.js 13" repo owned by trpc team
|
||||
// file link: https://github.com/trpc/next-13/blob/main/%40trpc/next-layout/createTRPCNextLayout.ts
|
||||
// repo link: https://github.com/trpc/next-13
|
||||
// code is / will continue to be adapted for our usage
|
||||
import { dehydrate, QueryClient } from "@tanstack/query-core";
|
||||
import type { DehydratedState, QueryKey } from "@tanstack/react-query";
|
||||
|
||||
import type { Maybe, TRPCClientError, TRPCClientErrorLike } from "@calcom/trpc";
|
||||
import {
|
||||
callProcedure,
|
||||
type AnyProcedure,
|
||||
type AnyQueryProcedure,
|
||||
type AnyRouter,
|
||||
type DataTransformer,
|
||||
type inferProcedureInput,
|
||||
type inferProcedureOutput,
|
||||
type inferRouterContext,
|
||||
type MaybePromise,
|
||||
type ProcedureRouterRecord,
|
||||
} from "@calcom/trpc/server";
|
||||
|
||||
import { createRecursiveProxy, createFlatProxy } from "@trpc/server/shared";
|
||||
|
||||
export function getArrayQueryKey(
|
||||
queryKey: string | [string] | [string, ...unknown[]] | unknown[],
|
||||
type: string
|
||||
): QueryKey {
|
||||
const queryKeyArrayed = Array.isArray(queryKey) ? queryKey : [queryKey];
|
||||
const [arrayPath, input] = queryKeyArrayed;
|
||||
|
||||
if (!input && (!type || type === "any")) {
|
||||
return Array.isArray(arrayPath) && arrayPath.length !== 0 ? [arrayPath] : ([] as unknown as QueryKey);
|
||||
}
|
||||
|
||||
return [
|
||||
arrayPath,
|
||||
{
|
||||
...(typeof input !== "undefined" && { input: input }),
|
||||
...(type && type !== "any" && { type: type }),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// copy starts
|
||||
// copied from trpc/trpc repo
|
||||
// ref: https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L37-#L58
|
||||
function transformQueryOrMutationCacheErrors<
|
||||
TState extends DehydratedState["queries"][0] | DehydratedState["mutations"][0]
|
||||
>(result: TState): TState {
|
||||
const error = result.state.error as Maybe<TRPCClientError<any>>;
|
||||
if (error instanceof Error && error.name === "TRPCClientError") {
|
||||
const newError: TRPCClientErrorLike<any> = {
|
||||
message: error.message,
|
||||
data: error.data,
|
||||
shape: error.shape,
|
||||
};
|
||||
return {
|
||||
...result,
|
||||
state: {
|
||||
...result.state,
|
||||
error: newError,
|
||||
},
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// copy ends
|
||||
|
||||
interface CreateTRPCNextLayoutOptions<TRouter extends AnyRouter> {
|
||||
router: TRouter;
|
||||
createContext: () => MaybePromise<inferRouterContext<TRouter>>;
|
||||
transformer?: DataTransformer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type DecorateProcedure<TProcedure extends AnyProcedure> = TProcedure extends AnyQueryProcedure
|
||||
? {
|
||||
fetch(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
|
||||
fetchInfinite(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
|
||||
prefetch(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
|
||||
prefetchInfinite(input: inferProcedureInput<TProcedure>): Promise<inferProcedureOutput<TProcedure>>;
|
||||
}
|
||||
: never;
|
||||
|
||||
type OmitNever<TType> = Pick<
|
||||
TType,
|
||||
{
|
||||
[K in keyof TType]: TType[K] extends never ? never : K;
|
||||
}[keyof TType]
|
||||
>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type DecoratedProcedureRecord<
|
||||
TProcedures extends ProcedureRouterRecord,
|
||||
TPath extends string = ""
|
||||
> = OmitNever<{
|
||||
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
|
||||
? DecoratedProcedureRecord<TProcedures[TKey]["_def"]["record"], `${TPath}${TKey & string}.`>
|
||||
: TProcedures[TKey] extends AnyQueryProcedure
|
||||
? DecorateProcedure<TProcedures[TKey]>
|
||||
: never;
|
||||
}>;
|
||||
|
||||
type CreateTRPCNextLayout<TRouter extends AnyRouter> = DecoratedProcedureRecord<TRouter["_def"]["record"]> & {
|
||||
dehydrate(): Promise<DehydratedState>;
|
||||
queryClient: QueryClient;
|
||||
};
|
||||
|
||||
const getStateContainer = <TRouter extends AnyRouter>(opts: CreateTRPCNextLayoutOptions<TRouter>) => {
|
||||
let _trpc: {
|
||||
queryClient: QueryClient;
|
||||
context: inferRouterContext<TRouter>;
|
||||
} | null = null;
|
||||
|
||||
return () => {
|
||||
if (_trpc === null) {
|
||||
_trpc = {
|
||||
context: opts.createContext(),
|
||||
queryClient: new QueryClient(),
|
||||
};
|
||||
}
|
||||
|
||||
return _trpc;
|
||||
};
|
||||
};
|
||||
|
||||
export function createTRPCNextLayout<TRouter extends AnyRouter>(
|
||||
opts: CreateTRPCNextLayoutOptions<TRouter>
|
||||
): CreateTRPCNextLayout<TRouter> {
|
||||
const getState = getStateContainer(opts);
|
||||
|
||||
const transformer = opts.transformer ?? {
|
||||
serialize: (v) => v,
|
||||
deserialize: (v) => v,
|
||||
};
|
||||
|
||||
return createFlatProxy((key) => {
|
||||
const state = getState();
|
||||
const { queryClient } = state;
|
||||
if (key === "queryClient") {
|
||||
return queryClient;
|
||||
}
|
||||
|
||||
if (key === "dehydrate") {
|
||||
// copy starts
|
||||
// copied from trpc/trpc repo
|
||||
// ref: https://github.com/trpc/trpc/blob/main/packages/next/src/withTRPC.tsx#L214-#L229
|
||||
const dehydratedCache = dehydrate(queryClient, {
|
||||
shouldDehydrateQuery() {
|
||||
// makes sure errors are also dehydrated
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
// since error instances can't be serialized, let's make them into `TRPCClientErrorLike`-objects
|
||||
const dehydratedCacheWithErrors = {
|
||||
...dehydratedCache,
|
||||
queries: dehydratedCache.queries.map(transformQueryOrMutationCacheErrors),
|
||||
mutations: dehydratedCache.mutations.map(transformQueryOrMutationCacheErrors),
|
||||
};
|
||||
|
||||
return () => transformer.serialize(dehydratedCacheWithErrors);
|
||||
}
|
||||
// copy ends
|
||||
|
||||
return createRecursiveProxy(async (callOpts) => {
|
||||
const path = [key, ...callOpts.path];
|
||||
const utilName = path.pop();
|
||||
const ctx = await state.context;
|
||||
|
||||
const caller = opts.router.createCaller(ctx);
|
||||
|
||||
const pathStr = path.join(".");
|
||||
const input = callOpts.args[0];
|
||||
|
||||
if (utilName === "fetchInfinite") {
|
||||
return queryClient.fetchInfiniteQuery(getArrayQueryKey([path, input], "infinite"), () =>
|
||||
caller.query(pathStr, input)
|
||||
);
|
||||
}
|
||||
|
||||
if (utilName === "prefetch") {
|
||||
return queryClient.prefetchQuery({
|
||||
queryKey: getArrayQueryKey([path, input], "query"),
|
||||
queryFn: async () => {
|
||||
const res = await callProcedure({
|
||||
procedures: opts.router._def.procedures,
|
||||
path: pathStr,
|
||||
rawInput: input,
|
||||
ctx,
|
||||
type: "query",
|
||||
});
|
||||
return res;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (utilName === "prefetchInfinite") {
|
||||
return queryClient.prefetchInfiniteQuery(getArrayQueryKey([path, input], "infinite"), () =>
|
||||
caller.query(pathStr, input)
|
||||
);
|
||||
}
|
||||
|
||||
return queryClient.fetchQuery(getArrayQueryKey([path, input], "query"), () =>
|
||||
caller.query(pathStr, input)
|
||||
);
|
||||
}) as CreateTRPCNextLayout<TRouter>;
|
||||
});
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import { headers } from "next/headers";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { CALCOM_VERSION } from "@calcom/lib/constants";
|
||||
import prisma, { readonlyPrisma } from "@calcom/prisma";
|
||||
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
import { createTRPCNextLayout } from "./createTRPCNextLayout";
|
||||
|
||||
export async function ssgInit() {
|
||||
const locale = headers().get("x-locale") ?? "en";
|
||||
|
||||
const i18n = (await serverSideTranslations(locale, ["common"])) || "en";
|
||||
|
||||
const ssg = createTRPCNextLayout({
|
||||
router: appRouter,
|
||||
transformer: superjson,
|
||||
createContext() {
|
||||
return { prisma, insightsDb: readonlyPrisma, session: null, locale, i18n };
|
||||
},
|
||||
});
|
||||
|
||||
// i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch
|
||||
// we can set query data directly to the queryClient
|
||||
const queryKey = [
|
||||
["viewer", "public", "i18n"],
|
||||
{ input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" },
|
||||
];
|
||||
|
||||
ssg.queryClient.setQueryData(queryKey, { i18n });
|
||||
|
||||
return ssg;
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { type GetServerSidePropsContext } from "next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import { headers, cookies } from "next/headers";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import { CALCOM_VERSION } from "@calcom/lib/constants";
|
||||
import prisma, { readonlyPrisma } from "@calcom/prisma";
|
||||
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
import { createTRPCNextLayout } from "./createTRPCNextLayout";
|
||||
|
||||
export async function ssrInit(options?: { noI18nPreload: boolean }) {
|
||||
const req = {
|
||||
headers: headers(),
|
||||
cookies: cookies(),
|
||||
};
|
||||
|
||||
const locale = await getLocale(req);
|
||||
|
||||
const i18n = (await serverSideTranslations(locale, ["common", "vital"])) || "en";
|
||||
|
||||
const ssr = createTRPCNextLayout({
|
||||
router: appRouter,
|
||||
transformer: superjson,
|
||||
createContext() {
|
||||
return {
|
||||
prisma,
|
||||
insightsDb: readonlyPrisma,
|
||||
session: null,
|
||||
locale,
|
||||
i18n,
|
||||
req: req as unknown as GetServerSidePropsContext["req"],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch
|
||||
// we can set query data directly to the queryClient
|
||||
const queryKey = [
|
||||
["viewer", "public", "i18n"],
|
||||
{ input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" },
|
||||
];
|
||||
if (!options?.noI18nPreload) {
|
||||
ssr.queryClient.setQueryData(queryKey, { i18n });
|
||||
}
|
||||
|
||||
await Promise.allSettled([
|
||||
// So feature flags are available on first render
|
||||
ssr.viewer.features.map.prefetch(),
|
||||
// Provides a better UX to the users who have already upgraded.
|
||||
ssr.viewer.teams.hasTeamPlan.prefetch(),
|
||||
ssr.viewer.public.session.prefetch(),
|
||||
]);
|
||||
|
||||
return ssr;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { type ReactElement } from "react";
|
||||
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
type EventTypesLayoutProps = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export default function Layout({ children }: EventTypesLayoutProps) {
|
||||
return (
|
||||
<PageWrapper getLayout={null} requiresLicense={false} nonce={undefined} themeBasis={null}>
|
||||
{children}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
import AppPage from "@pages/apps/[slug]/index";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import fs from "fs";
|
||||
import matter from "gray-matter";
|
||||
import { notFound } from "next/navigation";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||
import { getAppAssetFullPath } from "@calcom/app-store/getAppAssetFullPath";
|
||||
import { APP_NAME, IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const sourceSchema = z.object({
|
||||
content: z.string(),
|
||||
data: z.object({
|
||||
description: z.string().optional(),
|
||||
items: z
|
||||
.array(
|
||||
z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
iframe: z.object({ src: z.string() }),
|
||||
}),
|
||||
])
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const { data } = await getPageProps({ params });
|
||||
|
||||
return await _generateMetadata(
|
||||
() => `${data.name} | ${APP_NAME}`,
|
||||
() => data.description
|
||||
);
|
||||
};
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
try {
|
||||
const appStore = await prisma.app.findMany({ select: { slug: true } });
|
||||
return appStore.map(({ slug }) => ({ slug }));
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Prisma.PrismaClientInitializationError) {
|
||||
// Database is not available at build time, but that's ok – we fall back to resolving paths on demand
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
if (typeof params?.slug !== "string") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const appMeta = await getAppWithMetadata({
|
||||
slug: params?.slug,
|
||||
});
|
||||
|
||||
const appFromDb = await prisma.app.findUnique({
|
||||
where: { slug: params.slug.toLowerCase() },
|
||||
});
|
||||
|
||||
const isAppAvailableInFileSystem = appMeta;
|
||||
const isAppDisabled = isAppAvailableInFileSystem && (!appFromDb || !appFromDb.enabled);
|
||||
|
||||
if (!IS_PRODUCTION && isAppDisabled) {
|
||||
return {
|
||||
isAppDisabled: true as const,
|
||||
data: {
|
||||
...appMeta,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!appFromDb || !appMeta || isAppDisabled) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const isTemplate = appMeta.isTemplate;
|
||||
const appDirname = path.join(isTemplate ? "templates" : "", appFromDb.dirName);
|
||||
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
|
||||
const postFilePath = path.join(README_PATH);
|
||||
let source = "";
|
||||
|
||||
try {
|
||||
source = fs.readFileSync(postFilePath).toString();
|
||||
source = source.replace(/{DESCRIPTION}/g, appMeta.description);
|
||||
} catch (error) {
|
||||
/* If the app doesn't have a README we fallback to the package description */
|
||||
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
|
||||
source = appMeta.description;
|
||||
}
|
||||
|
||||
const result = matter(source);
|
||||
const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
|
||||
if (data.items) {
|
||||
data.items = data.items.map((item) => {
|
||||
if (typeof item === "string") {
|
||||
return getAppAssetFullPath(item, {
|
||||
dirName: appMeta.dirName,
|
||||
isTemplate: appMeta.isTemplate,
|
||||
});
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return {
|
||||
isAppDisabled: false as const,
|
||||
source: { content, data },
|
||||
data: appMeta,
|
||||
};
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||
const pageProps = await getPageProps({ params });
|
||||
|
||||
return <AppPage {...pageProps} />;
|
||||
}
|
||||
|
||||
export const dynamic = "force-static";
|
|
@ -0,0 +1,36 @@
|
|||
import SetupPage from "@pages/apps/[slug]/setup";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
return await _generateMetadata(
|
||||
() => `${params.slug} | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const req = { headers: headers(), cookies: cookies() };
|
||||
|
||||
const result = await getServerSideProps({ params, req } as unknown as GetServerSidePropsContext);
|
||||
|
||||
if (!result || "notFound" in result) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if ("redirect" in result) {
|
||||
redirect(result.redirect.destination);
|
||||
}
|
||||
|
||||
return result.props;
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||
const pageProps = await getPageProps({ params });
|
||||
return <SetupPage {...pageProps} />;
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import CategoryPage from "@pages/apps/categories/[category]";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
import z from "zod";
|
||||
|
||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => `${APP_NAME} | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
export const generateStaticParams = async () => {
|
||||
const paths = Object.keys(AppCategories);
|
||||
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1`;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Prisma.PrismaClientInitializationError) {
|
||||
// Database is not available at build time. Make sure we fall back to building these pages on demand
|
||||
return [];
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return paths.map((category) => ({ category }));
|
||||
};
|
||||
|
||||
const querySchema = z.object({
|
||||
category: z.nativeEnum(AppCategories),
|
||||
});
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const p = querySchema.safeParse(params);
|
||||
|
||||
if (!p.success) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const appQuery = await prisma.app.findMany({
|
||||
where: {
|
||||
categories: {
|
||||
has: p.data.category,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
});
|
||||
|
||||
const dbAppsSlugs = appQuery.map((category) => category.slug);
|
||||
|
||||
const appStore = await getAppRegistry();
|
||||
|
||||
const apps = appStore.filter((app) => dbAppsSlugs.includes(app.slug));
|
||||
return {
|
||||
apps,
|
||||
};
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||
const { apps } = await getPageProps({ params });
|
||||
return (
|
||||
<PageWrapper getLayout={null} requiresLicense={false} nonce={undefined} themeBasis={null}>
|
||||
<CategoryPage apps={apps} />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export const dynamic = "force-static";
|
|
@ -0,0 +1,56 @@
|
|||
import LegacyPage from "@pages/apps/categories/index";
|
||||
import { ssrInit } from "app/_trpc/ssrInit";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
() => `Categories | ${APP_NAME}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
async function getPageProps() {
|
||||
const ssr = await ssrInit();
|
||||
const req = { headers: headers(), cookies: cookies() };
|
||||
|
||||
// @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest | IncomingMessage
|
||||
const session = await getServerSession({ req });
|
||||
|
||||
let appStore;
|
||||
if (session?.user?.id) {
|
||||
appStore = await getAppRegistryWithCredentials(session.user.id);
|
||||
} else {
|
||||
appStore = await getAppRegistry();
|
||||
}
|
||||
|
||||
const categories = appStore.reduce((c, app) => {
|
||||
for (const category of app.categories) {
|
||||
c[category] = c[category] ? c[category] + 1 : 1;
|
||||
}
|
||||
return c;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
return {
|
||||
categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
|
||||
dehydratedState: await ssr.dehydrate(),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page() {
|
||||
const props = await getPageProps();
|
||||
const h = headers();
|
||||
const nonce = h.get("x-nonce") ?? undefined;
|
||||
|
||||
return (
|
||||
<PageWrapper getLayout={null} requiresLicense={false} nonce={nonce} themeBasis={null} {...props}>
|
||||
<LegacyPage {...props} />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { type ReactElement } from "react";
|
||||
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
type EventTypesLayoutProps = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export default function Layout({ children }: EventTypesLayoutProps) {
|
||||
return (
|
||||
<PageWrapper getLayout={null} requiresLicense={false} nonce={undefined} themeBasis={null}>
|
||||
{children}
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import LegacyPage from "@pages/apps/installed/[category]";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
const querySchema = z.object({
|
||||
category: z.nativeEnum(AppCategories),
|
||||
});
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => `${t("installed_apps")} | ${APP_NAME}`,
|
||||
(t) => t("manage_your_connected_apps")
|
||||
);
|
||||
};
|
||||
|
||||
const getPageProps = async ({ params }: { params: Record<string, string | string[]> }) => {
|
||||
const p = querySchema.safeParse(params);
|
||||
|
||||
if (!p.success) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
category: p.data.category,
|
||||
};
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: Record<string, string | string[]> }) {
|
||||
const { category } = await getPageProps({ params });
|
||||
|
||||
return <LegacyPage />;
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
import Page from "@pages/settings/admin/organizations/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import Page from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("organizations"),
|
||||
(t) => t("orgs_page_description")
|
||||
);
|
||||
|
||||
export default function AppPage() {
|
||||
// @ts-expect-error FIXME Property 'Component' is incompatible with index signature
|
||||
return <Page />;
|
||||
}
|
||||
export default Page;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import Page from "@pages/settings/admin/users/[id]/edit";
|
||||
import { getServerCaller } from "app/_trpc/serverClient";
|
||||
import { type Params } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { z } from "zod";
|
||||
|
||||
import Page from "@calcom/features/ee/users/pages/users-edit-view";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const userIdSchema = z.object({ id: z.coerce.number() });
|
||||
|
@ -33,7 +33,4 @@ export const generateMetadata = async ({ params }: { params: Params }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default function AppPage() {
|
||||
// @ts-expect-error FIXME AppProps | undefined' does not satisfy the constraint 'PageProps'
|
||||
return <Page />;
|
||||
}
|
||||
export default Page;
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import Page from "@pages/settings/admin/users/add";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import Page from "@calcom/features/ee/users/pages/users-add-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "Add new user",
|
||||
() => "Here you can add a new user."
|
||||
);
|
||||
|
||||
export default function AppPage() {
|
||||
// @ts-expect-error FIXME AppProps | undefined' does not satisfy the constraint 'PageProps'
|
||||
return <Page />;
|
||||
}
|
||||
export default Page;
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import Page from "@pages/settings/admin/users/index";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
|
||||
import Page from "@calcom/features/ee/users/pages/users-listing-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "Users",
|
||||
() => "A list of all the users in your account including their name, title, email and role."
|
||||
);
|
||||
|
||||
export default function AppPage() {
|
||||
// @ts-expect-error FIXME Property 'Component' is incompatible with index signature
|
||||
return <Page />;
|
||||
}
|
||||
export default Page;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { dir } from "i18next";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
import { headers, cookies } from "next/headers";
|
||||
import Script from "next/script";
|
||||
import React from "react";
|
||||
|
@ -10,6 +12,14 @@ import { prepareRootMetadata } from "@lib/metadata";
|
|||
|
||||
import "../styles/globals.css";
|
||||
|
||||
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
||||
const calFont = localFont({
|
||||
src: "../fonts/CalSans-SemiBold.woff2",
|
||||
variable: "--font-cal",
|
||||
preload: true,
|
||||
display: "block",
|
||||
});
|
||||
|
||||
export const generateMetadata = () =>
|
||||
prepareRootMetadata({
|
||||
twitterCreator: "@calcom",
|
||||
|
@ -66,6 +76,12 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||
src="https://snippet.meticulous.ai/v1/stagingMeticulousSnippet.js"
|
||||
/>
|
||||
)}
|
||||
<style>{`
|
||||
:root {
|
||||
--font-inter: ${interFont.style.fontFamily.replace(/\'/g, "")};
|
||||
--font-cal: ${calFont.style.fontFamily.replace(/\'/g, "")};
|
||||
}
|
||||
`}</style>
|
||||
</head>
|
||||
<body
|
||||
className="dark:bg-darkgray-50 desktop-transparent bg-subtle antialiased"
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
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";
|
||||
// import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Script from "next/script";
|
||||
|
@ -20,14 +18,6 @@ export interface CalPageWrapper {
|
|||
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||
}
|
||||
|
||||
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
||||
const calFont = localFont({
|
||||
src: "../fonts/CalSans-SemiBold.woff2",
|
||||
variable: "--font-cal",
|
||||
preload: true,
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export type PageWrapperProps = Readonly<{
|
||||
getLayout: ((page: React.ReactElement) => ReactNode) | null;
|
||||
children: React.ReactElement;
|
||||
|
@ -71,13 +61,6 @@ function PageWrapper(props: PageWrapperProps) {
|
|||
id="page-status"
|
||||
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
||||
/>
|
||||
<style jsx global>{`
|
||||
:root {
|
||||
--font-inter: ${interFont.style.fontFamily};
|
||||
--font-cal: ${calFont.style.fontFamily};
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{getLayout(
|
||||
props.requiresLicense ? <LicenseRequired>{props.children}</LicenseRequired> : props.children
|
||||
)}
|
||||
|
|
|
@ -382,7 +382,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||
}}
|
||||
/>
|
||||
{selectedLocation && LocationOptions}
|
||||
<DialogFooter className="mt-4">
|
||||
<DialogFooter className="relative">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowLocationModal(false);
|
||||
|
|
|
@ -357,7 +357,6 @@ export const EventSetupTab = (
|
|||
<div className="flex">
|
||||
<LocationSelect
|
||||
defaultMenuIsOpen={showEmptyLocationSelect}
|
||||
autoFocus
|
||||
placeholder={t("select")}
|
||||
options={locationOptions}
|
||||
value={selectedNewOption}
|
||||
|
|
|
@ -156,11 +156,8 @@ const UserProfile = () => {
|
|||
{t("few_sentences_about_yourself")}
|
||||
</p>
|
||||
</fieldset>
|
||||
<Button
|
||||
type="submit"
|
||||
className="text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm">
|
||||
<Button EndIcon={ArrowRight} type="submit" className="mt-8 w-full items-center justify-center">
|
||||
{t("finish")}
|
||||
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -9,7 +9,7 @@ import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, TimezoneSelect } from "@calcom/ui";
|
||||
import { Button, TimezoneSelect, Input } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||
|
@ -76,7 +76,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||
<label htmlFor="name" className="text-default mb-2 block text-sm font-medium">
|
||||
{t("full_name")}
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
{...register("name", {
|
||||
required: true,
|
||||
})}
|
||||
|
@ -85,7 +85,6 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="border-default w-full rounded-md border text-sm"
|
||||
/>
|
||||
{errors.name && (
|
||||
<p data-testid="required" className="py-2 text-xs text-red-500">
|
||||
|
@ -106,7 +105,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||
className="mt-2 w-full rounded-md text-sm"
|
||||
/>
|
||||
|
||||
<p className="text-subtle dark:text-inverted mt-3 flex flex-row font-sans text-xs leading-tight">
|
||||
<p className="text-subtle mt-3 flex flex-row font-sans text-xs leading-tight">
|
||||
{t("current_time")} {dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -97,13 +97,9 @@ const CustomI18nextProvider = (props: { children: React.ReactElement; i18n?: SSR
|
|||
const clientViewerI18n = useViewerI18n(locale);
|
||||
const i18n = clientViewerI18n.data?.i18n ?? props.i18n;
|
||||
|
||||
if (!i18n || !i18n._nextI18Next) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
// @ts-expect-error AppWithTranslationHoc expects AppProps
|
||||
<AppWithTranslationHoc pageProps={{ _nextI18Next: i18n._nextI18Next }}>
|
||||
<AppWithTranslationHoc pageProps={{ _nextI18Next: i18n?._nextI18Next }}>
|
||||
{props.children}
|
||||
</AppWithTranslationHoc>
|
||||
);
|
||||
|
|
|
@ -64,6 +64,23 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
|
|||
requestHeaders.set("x-csp-enforce", "true");
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/future/apps/installed")) {
|
||||
const returnTo = req.cookies.get("return-to")?.value;
|
||||
if (returnTo !== undefined) {
|
||||
requestHeaders.set("Set-Cookie", "return-to=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT");
|
||||
|
||||
let validPathname = returnTo;
|
||||
|
||||
try {
|
||||
validPathname = new URL(returnTo).pathname;
|
||||
} catch (e) {}
|
||||
|
||||
const nextUrl = url.clone();
|
||||
nextUrl.pathname = validPathname;
|
||||
return NextResponse.redirect(nextUrl, { headers: requestHeaders });
|
||||
}
|
||||
}
|
||||
|
||||
requestHeaders.set("x-pathname", url.pathname);
|
||||
|
||||
const locale = await getLocale(req);
|
||||
|
@ -103,6 +120,16 @@ export const config = {
|
|||
"/future/event-types/",
|
||||
"/settings/admin/:path*",
|
||||
"/future/settings/admin/:path*",
|
||||
"/apps/installed/:category/",
|
||||
"/future/apps/installed/:category/",
|
||||
"/apps/:slug/",
|
||||
"/future/apps/:slug/",
|
||||
"/apps/:slug/setup/",
|
||||
"/future/apps/:slug/setup/",
|
||||
"/apps/categories/",
|
||||
"/future/apps/categories/",
|
||||
"/apps/categories/:category/",
|
||||
"/future/apps/categories/:category/",
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -154,11 +154,14 @@ const matcherConfigUserTypeEmbedRoute = {
|
|||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
serverComponentsExternalPackages: ["next-i18next"],
|
||||
},
|
||||
i18n: {
|
||||
...i18n,
|
||||
localeDetection: false,
|
||||
},
|
||||
productionBrowserSourceMaps: true,
|
||||
productionBrowserSourceMaps: false,
|
||||
/* We already do type check on GH actions */
|
||||
typescript: {
|
||||
ignoreBuildErrors: !!process.env.CI,
|
||||
|
@ -517,6 +520,11 @@ const nextConfig = {
|
|||
destination: "/apps/installed/conferencing",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/apps/installed",
|
||||
destination: "/apps/installed/calendar",
|
||||
permanent: true,
|
||||
},
|
||||
// OAuth callbacks when sent to localhost:3000(w would be expected) should be redirected to corresponding to WEBAPP_URL
|
||||
...(process.env.NODE_ENV === "development" &&
|
||||
// Safer to enable the redirect only when the user is opting to test out organizations
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.5.4",
|
||||
"version": "3.5.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
@ -83,7 +83,7 @@
|
|||
"ics": "^2.37.0",
|
||||
"jose": "^4.13.1",
|
||||
"kbar": "^0.1.0-beta.36",
|
||||
"libphonenumber-js": "^1.10.12",
|
||||
"libphonenumber-js": "^1.10.51",
|
||||
"lodash": "^4.17.21",
|
||||
"lottie-react": "^2.3.1",
|
||||
"markdown-it": "^13.0.1",
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import type { GetServerSidePropsContext } from "next";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
import {
|
||||
getBookingForReschedule,
|
||||
getBookingForSeatedEvent,
|
||||
getMultipleDurationValue,
|
||||
} from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { orgDomainConfig, userOrgQuery } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
|
@ -26,6 +23,16 @@ import { getTemporaryOrgRedirect } from "../../lib/getTemporaryOrgRedirect";
|
|||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
|
||||
export const getMultipleDurationValue = (
|
||||
multipleDurationConfig: number[] | undefined,
|
||||
queryDuration: string | string[] | null | undefined,
|
||||
defaultValue: number
|
||||
) => {
|
||||
if (!multipleDurationConfig) return null;
|
||||
if (multipleDurationConfig.includes(Number(queryDuration))) return Number(queryDuration);
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
export default function Type({
|
||||
slug,
|
||||
user,
|
||||
|
@ -35,9 +42,10 @@ export default function Type({
|
|||
isBrandingHidden,
|
||||
isSEOIndexable,
|
||||
rescheduleUid,
|
||||
entity,
|
||||
duration,
|
||||
eventData,
|
||||
}: PageProps) {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
return (
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
|
||||
<BookerSeo
|
||||
|
@ -46,7 +54,7 @@ export default function Type({
|
|||
rescheduleUid={rescheduleUid ?? undefined}
|
||||
hideBranding={isBrandingHidden}
|
||||
isSEOIndexable={isSEOIndexable ?? true}
|
||||
entity={entity}
|
||||
entity={eventData.entity}
|
||||
bookingData={booking}
|
||||
/>
|
||||
<Booker
|
||||
|
@ -55,8 +63,16 @@ export default function Type({
|
|||
bookingData={booking}
|
||||
isAway={away}
|
||||
hideBranding={isBrandingHidden}
|
||||
entity={entity}
|
||||
duration={duration}
|
||||
entity={eventData.entity}
|
||||
durationConfig={eventData.metadata?.multipleDuration}
|
||||
/* TODO: Currently unused, evaluate it is needed-
|
||||
* Possible alternative approach is to have onDurationChange.
|
||||
*/
|
||||
duration={getMultipleDurationValue(
|
||||
eventData.metadata?.multipleDuration,
|
||||
searchParams?.get("duration"),
|
||||
eventData.length
|
||||
)}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
@ -68,7 +84,7 @@ Type.PageWrapper = PageWrapper;
|
|||
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(context);
|
||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
|
||||
const { rescheduleUid, bookingUid } = context.query;
|
||||
|
||||
const { ssrInit } = await import("@server/lib/ssr");
|
||||
const ssr = await ssrInit(context);
|
||||
|
@ -120,12 +136,14 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
|||
|
||||
return {
|
||||
props: {
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
duration: getMultipleDurationValue(
|
||||
eventData.metadata?.multipleDuration,
|
||||
queryDuration,
|
||||
eventData.length
|
||||
),
|
||||
length: eventData.length,
|
||||
metadata: {
|
||||
...eventData.metadata,
|
||||
multipleDuration: [15, 30, 60],
|
||||
},
|
||||
},
|
||||
booking,
|
||||
user: usernames.join("+"),
|
||||
slug,
|
||||
|
@ -144,7 +162,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||
const session = await getServerSession(context);
|
||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||
const username = usernames[0];
|
||||
const { rescheduleUid, bookingUid, duration: queryDuration } = context.query;
|
||||
const { rescheduleUid, bookingUid } = context.query;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
|
||||
|
||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||
|
@ -207,15 +225,14 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||
return {
|
||||
props: {
|
||||
booking,
|
||||
duration: getMultipleDurationValue(
|
||||
eventData.metadata?.multipleDuration,
|
||||
queryDuration,
|
||||
eventData.length
|
||||
),
|
||||
eventData: {
|
||||
entity: eventData.entity,
|
||||
length: eventData.length,
|
||||
metadata: eventData.metadata,
|
||||
},
|
||||
away: user?.away,
|
||||
user: username,
|
||||
slug,
|
||||
entity: eventData.entity,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: user?.hideBranding,
|
||||
isSEOIndexable: user?.allowSEOIndexing,
|
||||
|
|
|
@ -6,8 +6,18 @@ import { type RequestWithUsernameStatus } from "@calcom/features/auth/signup/use
|
|||
import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { signupSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
function ensureSignupIsEnabled(req: RequestWithUsernameStatus) {
|
||||
const { token } = signupSchema
|
||||
.pick({
|
||||
token: true,
|
||||
})
|
||||
.parse(req.body);
|
||||
|
||||
// Stil allow signups if there is a team invite
|
||||
if (token) return;
|
||||
|
||||
function ensureSignupIsEnabled() {
|
||||
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") {
|
||||
throw new HttpError({
|
||||
statusCode: 403,
|
||||
|
@ -29,7 +39,7 @@ export default async function handler(req: RequestWithUsernameStatus, res: NextA
|
|||
// Use a try catch instead of returning res every time
|
||||
try {
|
||||
ensureReqIsPost(req);
|
||||
ensureSignupIsEnabled();
|
||||
ensureSignupIsEnabled(req);
|
||||
|
||||
/**
|
||||
* Im not sure its worth merging these two handlers. They are different enough to be separate.
|
||||
|
|
|
@ -1,24 +1,38 @@
|
|||
import type { WebhookTriggerEvents } from "@prisma/client";
|
||||
import { createHmac } from "crypto";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { DailyLocationType } from "@calcom/app-store/locations";
|
||||
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
|
||||
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
const schema = z.object({
|
||||
recordingId: z.string(),
|
||||
bookingUID: z.string(),
|
||||
});
|
||||
const schema = z
|
||||
.object({
|
||||
version: z.string(),
|
||||
type: z.string(),
|
||||
id: z.string(),
|
||||
payload: z.object({
|
||||
recording_id: z.string(),
|
||||
end_ts: z.number(),
|
||||
room_name: z.string(),
|
||||
start_ts: z.number(),
|
||||
status: z.string(),
|
||||
|
||||
max_participants: z.number(),
|
||||
duration: z.number(),
|
||||
s3_key: z.string(),
|
||||
}),
|
||||
event_ts: z.number(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const downloadLinkSchema = z.object({
|
||||
download_link: z.string(),
|
||||
|
@ -39,8 +53,8 @@ const triggerWebhook = async ({
|
|||
};
|
||||
}) => {
|
||||
const eventTrigger: WebhookTriggerEvents = "RECORDING_READY";
|
||||
// Send Webhook call if hooked to BOOKING.RECORDING_READY
|
||||
|
||||
// Send Webhook call if hooked to BOOKING.RECORDING_READY
|
||||
const triggerForUser = !booking.teamId || (booking.teamId && booking.eventTypeParentId);
|
||||
|
||||
const subscriberOptions = {
|
||||
|
@ -62,71 +76,62 @@ const triggerWebhook = async ({
|
|||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
const checkIfUserIsPartOfTheSameTeam = async (
|
||||
teamId: number | undefined | null,
|
||||
userId: number,
|
||||
userEmail: string | undefined | null
|
||||
) => {
|
||||
if (!teamId) return false;
|
||||
|
||||
const getUserQuery = () => {
|
||||
if (!!userEmail) {
|
||||
return {
|
||||
OR: [
|
||||
{
|
||||
id: userId,
|
||||
},
|
||||
{
|
||||
email: userEmail,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
id: userId,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
user: getUserQuery(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return !!team;
|
||||
};
|
||||
const testRequestSchema = z.object({
|
||||
test: z.enum(["test"]),
|
||||
});
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
||||
return res.status(405).json({ message: "No SendGrid API key or email" });
|
||||
}
|
||||
const response = schema.safeParse(JSON.parse(req.body));
|
||||
|
||||
if (!response.success) {
|
||||
if (testRequestSchema.safeParse(req.body).success) {
|
||||
return res.status(200).json({ message: "Test request successful" });
|
||||
}
|
||||
|
||||
const hmacSecret = process.env.DAILY_WEBHOOK_SECRET;
|
||||
if (!hmacSecret) {
|
||||
return res.status(405).json({ message: "No Daily Webhook Secret" });
|
||||
}
|
||||
|
||||
const signature = `${req.headers["x-webhook-timestamp"]}.${JSON.stringify(req.body)}`;
|
||||
const base64DecodedSecret = Buffer.from(hmacSecret, "base64");
|
||||
const hmac = createHmac("sha256", base64DecodedSecret);
|
||||
const computed_signature = hmac.update(signature).digest("base64");
|
||||
|
||||
if (req.headers["x-webhook-signature"] !== computed_signature) {
|
||||
return res.status(403).json({ message: "Signature does not match" });
|
||||
}
|
||||
|
||||
const response = schema.safeParse(req.body);
|
||||
|
||||
if (!response.success || response.data.type !== "recording.ready-to-download") {
|
||||
return res.status(400).send({
|
||||
message: "Invalid Payload",
|
||||
});
|
||||
}
|
||||
|
||||
const { recordingId, bookingUID } = response.data;
|
||||
const session = await getServerSession({ req, res });
|
||||
const { room_name, recording_id, status } = response.data.payload;
|
||||
|
||||
if (!session?.user) {
|
||||
return res.status(401).send({
|
||||
message: "User not logged in",
|
||||
if (status !== "finished") {
|
||||
return res.status(400).send({
|
||||
message: "Recording not finished",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const booking = await prisma.booking.findFirst({
|
||||
const bookingReference = await prisma.bookingReference.findFirst({
|
||||
where: { type: "daily_video", uid: room_name, meetingId: room_name },
|
||||
select: { bookingId: true },
|
||||
});
|
||||
|
||||
if (!bookingReference || !bookingReference.bookingId) {
|
||||
return res.status(404).send({ message: "Booking reference not found" });
|
||||
}
|
||||
|
||||
const booking = await prisma.booking.findUniqueOrThrow({
|
||||
where: {
|
||||
uid: bookingUID,
|
||||
id: bookingReference.bookingId,
|
||||
},
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
|
@ -153,9 +158,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
},
|
||||
});
|
||||
|
||||
if (!booking || booking.location !== DailyLocationType) {
|
||||
if (!booking || !(booking.location === DailyLocationType || booking?.location?.trim() === "")) {
|
||||
return res.status(404).send({
|
||||
message: `Booking of uid ${bookingUID} does not exist or does not contain daily video as location`,
|
||||
message: `Booking of room_name ${room_name} does not exist or does not contain daily video as location`,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -175,26 +180,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
|
||||
const attendeesList = await Promise.all(attendeesListPromises);
|
||||
|
||||
const isUserAttendeeOrOrganiser =
|
||||
booking?.user?.id === session.user.id ||
|
||||
attendeesList.find(
|
||||
(attendee) => attendee.id === session.user.id || attendee.email === session.user.email
|
||||
);
|
||||
|
||||
if (!isUserAttendeeOrOrganiser) {
|
||||
const isUserMemberOfTheTeam = checkIfUserIsPartOfTheSameTeam(
|
||||
booking?.eventType?.teamId,
|
||||
session.user.id,
|
||||
session.user.email
|
||||
);
|
||||
|
||||
if (!isUserMemberOfTheTeam) {
|
||||
return res.status(403).send({
|
||||
message: "Unauthorised",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid: booking.uid,
|
||||
|
@ -204,7 +189,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
},
|
||||
});
|
||||
|
||||
const response = await getDownloadLinkOfCalVideoByRecordingId(recordingId);
|
||||
const response = await getDownloadLinkOfCalVideoByRecordingId(recording_id);
|
||||
const downloadLinkResponse = downloadLinkSchema.parse(response);
|
||||
const downloadLink = downloadLinkResponse.download_link;
|
||||
|
||||
|
@ -242,17 +227,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
},
|
||||
});
|
||||
|
||||
const isSendingEmailsAllowed = IS_SELF_HOSTED || session?.user?.belongsToActiveTeam;
|
||||
|
||||
// send emails to all attendees only when user has team plan
|
||||
if (isSendingEmailsAllowed) {
|
||||
await sendDailyVideoRecordingEmails(evt, downloadLink);
|
||||
return res.status(200).json({ message: "Success" });
|
||||
}
|
||||
|
||||
return res.status(403).json({ message: "User does not have team plan to send out emails" });
|
||||
} catch (err) {
|
||||
console.warn("Error in /recorded-daily-video", err);
|
||||
console.error("Error in /recorded-daily-video", err);
|
||||
return res.status(500).json({ message: "something went wrong" });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
import fs from "fs";
|
||||
import matter from "gray-matter";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { GetStaticPropsContext, InferGetStaticPropsType } from "next";
|
||||
import Link from "next/link";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
|
@ -13,7 +15,7 @@ import PageWrapper from "@components/PageWrapper";
|
|||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export default function Apps({ categories }: inferSSRProps<typeof getServerSideProps>) {
|
||||
export default function Apps({ categories }: Omit<inferSSRProps<typeof getServerSideProps>, "trpcState">) {
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { useReducer } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import type { GetServerSidePropsContext } from "next";
|
|||
import { getCsrfToken, signIn } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { FaGoogle } from "react-icons/fa";
|
||||
|
@ -174,15 +173,7 @@ inferSSRProps<typeof _getServerSideProps> & WithNonceProps<{}>) {
|
|||
: isSAMLLoginEnabled && !isLoading && data?.connectionExists;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--cal-brand": "#111827",
|
||||
"--cal-brand-emphasis": "#101010",
|
||||
"--cal-brand-text": "white",
|
||||
"--cal-brand-subtle": "#9CA3AF",
|
||||
} as CSSProperties
|
||||
}>
|
||||
<div className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand-emphasis:#101010] [--cal-brand-subtle:9CA3AF] [--cal-brand-text:white] [--cal-brand:#111827] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand-text:black] dark:[--cal-brand:white]">
|
||||
<AuthContainer
|
||||
title={t("login")}
|
||||
description={t("login")}
|
||||
|
@ -238,7 +229,7 @@ inferSSRProps<typeof _getServerSideProps> & WithNonceProps<{}>) {
|
|||
type="submit"
|
||||
color="primary"
|
||||
disabled={formState.isSubmitting}
|
||||
className="w-full justify-center dark:bg-white dark:text-black">
|
||||
className="w-full justify-center">
|
||||
{twoFactorRequired ? t("submit") : t("sign_in")}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { GetStaticPaths, GetStaticProps } from "next";
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, useState } from "react";
|
||||
import React from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getLayout } from "@calcom/features/MainLayout";
|
||||
import { FilterToggle } from "@calcom/features/bookings/components/FilterToggle";
|
||||
import { FiltersContainer } from "@calcom/features/bookings/components/FiltersContainer";
|
||||
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
|
||||
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
|
||||
|
@ -81,6 +82,7 @@ export default function Bookings() {
|
|||
const { status } = params ? querySchema.parse(params) : { status: "upcoming" as const };
|
||||
const { t } = useLocale();
|
||||
const user = useMeQuery().data;
|
||||
const [isFiltersVisible, setIsFiltersVisible] = useState<boolean>(false);
|
||||
|
||||
const query = trpc.viewer.bookings.get.useInfiniteQuery(
|
||||
{
|
||||
|
@ -151,12 +153,11 @@ export default function Bookings() {
|
|||
return (
|
||||
<ShellMain hideHeadingOnMobile heading={t("bookings")} subtitle={t("bookings_description")}>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col flex-wrap lg:flex-row">
|
||||
<div className="flex flex-row flex-wrap justify-between">
|
||||
<HorizontalTabs tabs={tabs} />
|
||||
<div className="max-w-full overflow-x-auto xl:ml-auto">
|
||||
<FiltersContainer />
|
||||
</div>
|
||||
<FilterToggle setIsFiltersVisible={setIsFiltersVisible} />
|
||||
</div>
|
||||
<FiltersContainer isFiltersVisible={isFiltersVisible} />
|
||||
<main className="w-full">
|
||||
<div className="flex w-full flex-col" ref={animationParentRef}>
|
||||
{query.status === "error" && (
|
||||
|
|
|
@ -2,7 +2,6 @@ import type { GetServerSidePropsContext } from "next";
|
|||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import Head from "next/head";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import type { CSSProperties } from "react";
|
||||
import { Suspense } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
|
@ -106,16 +105,8 @@ const OnboardingPage = () => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen"
|
||||
className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand-emphasis:#101010] [--cal-brand-subtle:9CA3AF] [--cal-brand:#111827] [--cal-brand-text:#FFFFFF] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand:white] dark:[--cal-brand-text:#000000]"
|
||||
data-testid="onboarding"
|
||||
style={
|
||||
{
|
||||
"--cal-brand": "#111827",
|
||||
"--cal-brand-emphasis": "#101010",
|
||||
"--cal-brand-text": "white",
|
||||
"--cal-brand-subtle": "#9CA3AF",
|
||||
} as CSSProperties
|
||||
}
|
||||
key={pathname}>
|
||||
<Head>
|
||||
<title>{`${APP_NAME} - ${t("getting_started")}`}</title>
|
||||
|
@ -231,7 +222,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
};
|
||||
};
|
||||
|
||||
OnboardingPage.isThemeSupported = false;
|
||||
OnboardingPage.PageWrapper = PageWrapper;
|
||||
|
||||
export default OnboardingPage;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import type { GetServerSidePropsContext } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
@ -10,21 +12,29 @@ import UserTypePage, { getServerSideProps as GSSUserTypePage } from "../../../[u
|
|||
import type { PageProps as TeamTypePageProps } from "../../../team/[slug]/[type]";
|
||||
import TeamTypePage, { getServerSideProps as GSSTeamTypePage } from "../../../team/[slug]/[type]";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
orgSlug: z.string().transform((s) => slugify(s)),
|
||||
user: z.string().transform((s) => slugify(s)),
|
||||
type: z.string().transform((s) => slugify(s)),
|
||||
});
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
const { user: teamOrUserSlug, orgSlug, type } = paramsSchema.parse(ctx.params);
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: ctx.query.user as string,
|
||||
slug: teamOrUserSlug,
|
||||
parentId: {
|
||||
not: null,
|
||||
},
|
||||
parent: getSlugOrRequestedSlug(ctx.query.orgSlug as string),
|
||||
parent: getSlugOrRequestedSlug(orgSlug),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (team) {
|
||||
const params = { slug: ctx.query.user, type: ctx.query.type };
|
||||
const params = { slug: teamOrUserSlug, type };
|
||||
return GSSTeamTypePage({
|
||||
...ctx,
|
||||
params: {
|
||||
|
@ -37,7 +47,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||
},
|
||||
});
|
||||
}
|
||||
const params = { user: ctx.query.user, type: ctx.query.type };
|
||||
const params = { user: teamOrUserSlug, type };
|
||||
return GSSUserTypePage({
|
||||
...ctx,
|
||||
params: {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
"use client";
|
||||
|
||||
import AdminOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
"use client";
|
||||
|
||||
import UsersEditView from "@calcom/features/ee/users/pages/users-edit-view";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
"use client";
|
||||
|
||||
import UsersAddView from "@calcom/features/ee/users/pages/users-add-view";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
"use client";
|
||||
|
||||
import UsersListingView from "@calcom/features/ee/users/pages/users-listing-view";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
|
|
|
@ -106,8 +106,17 @@ const ProfileView = () => {
|
|||
setConfirmAuthEmailChangeWarningDialogOpen(false);
|
||||
setTempFormValues(null);
|
||||
},
|
||||
onError: () => {
|
||||
onError: (e) => {
|
||||
switch (e.message) {
|
||||
// TODO: Add error codes.
|
||||
case "email_already_used":
|
||||
{
|
||||
showToast(t(e.message), "error");
|
||||
}
|
||||
return;
|
||||
default:
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import type { GetServerSidePropsContext } from "next";
|
|||
import { signIn } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import type { SubmitHandler } from "react-hook-form";
|
||||
import { useForm, useFormContext } from "react-hook-form";
|
||||
|
@ -238,18 +237,10 @@ export default function Signup({
|
|||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="light bg-muted 2xl:bg-default flex min-h-screen w-full flex-col items-center justify-center"
|
||||
style={
|
||||
{
|
||||
"--cal-brand": "#111827",
|
||||
"--cal-brand-emphasis": "#101010",
|
||||
"--cal-brand-text": "white",
|
||||
"--cal-brand-subtle": "#9CA3AF",
|
||||
} as CSSProperties
|
||||
}>
|
||||
<div className="bg-muted 2xl:border-subtle grid w-full max-w-[1440px] grid-cols-1 grid-rows-1 lg:grid-cols-2 2xl:rounded-[20px] 2xl:border 2xl:py-6">
|
||||
<div className="light bg-muted 2xl:bg-default flex min-h-screen w-full flex-col items-center justify-center [--cal-brand-emphasis:#101010] [--cal-brand:#111827] [--cal-brand-text:#FFFFFF] [--cal-brand-subtle:#9CA3AF] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand:white] dark:[--cal-brand-text:#000000]">
|
||||
<div className="bg-muted 2xl:border-subtle grid w-full max-w-[1440px] grid-cols-1 grid-rows-1 overflow-hidden lg:grid-cols-2 2xl:rounded-[20px] 2xl:border 2xl:py-6">
|
||||
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
|
||||
{/* Left side */}
|
||||
<div className="flex w-full flex-col px-4 pt-6 sm:px-16 md:px-20 2xl:px-28">
|
||||
{/* Header */}
|
||||
{errors.apiError && (
|
||||
|
@ -354,7 +345,10 @@ export default function Signup({
|
|||
StartIcon={() => (
|
||||
<>
|
||||
<img
|
||||
className={classNames("text-subtle mr-2 h-4 w-4", premiumUsername && "opacity-50")}
|
||||
className={classNames(
|
||||
"text-subtle mr-2 h-4 w-4 dark:invert",
|
||||
premiumUsername && "opacity-50"
|
||||
)}
|
||||
src="/google-icon.svg"
|
||||
alt=""
|
||||
/>
|
||||
|
@ -525,7 +519,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||
// username + email prepopulated from query params
|
||||
const { username: preFillusername, email: prefilEmail } = querySchema.parse(ctx.query);
|
||||
|
||||
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" || flags["disable-signup"]) {
|
||||
if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || flags["disable-signup"]) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
|
@ -648,5 +642,4 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||
};
|
||||
};
|
||||
|
||||
Signup.isThemeSupported = false;
|
||||
Signup.PageWrapper = PageWrapper;
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import type { DailyEventObjectRecordingStarted } from "@daily-co/daily-js";
|
||||
import DailyIframe from "@daily-co/daily-js";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Head from "next/head";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import z from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
|
@ -21,19 +19,12 @@ import PageWrapper from "@components/PageWrapper";
|
|||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const recordingStartedEventResponse = z
|
||||
.object({
|
||||
recordingId: z.string(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export type JoinCallPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
export default function JoinCall(props: JoinCallPageProps) {
|
||||
const { t } = useLocale();
|
||||
const { meetingUrl, meetingPassword, booking } = props;
|
||||
const recordingId = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const callFrame = DailyIframe.createFrame({
|
||||
|
@ -61,31 +52,12 @@ export default function JoinCall(props: JoinCallPageProps) {
|
|||
...(typeof meetingPassword === "string" && { token: meetingPassword }),
|
||||
});
|
||||
callFrame.join();
|
||||
callFrame.on("recording-started", onRecordingStarted).on("recording-stopped", onRecordingStopped);
|
||||
return () => {
|
||||
callFrame.destroy();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onRecordingStopped = () => {
|
||||
const data = { recordingId: recordingId.current, bookingUID: booking.uid };
|
||||
|
||||
fetch("/api/recorded-daily-video", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
}).catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
recordingId.current = null;
|
||||
};
|
||||
|
||||
const onRecordingStarted = (event?: DailyEventObjectRecordingStarted | undefined) => {
|
||||
const response = recordingStartedEventResponse.parse(event);
|
||||
recordingId.current = response.recordingId;
|
||||
};
|
||||
|
||||
const title = `${APP_NAME} Video`;
|
||||
return (
|
||||
<>
|
||||
|
@ -104,15 +76,27 @@ export default function JoinCall(props: JoinCallPageProps) {
|
|||
<meta property="twitter:description" content={t("quick_video_meeting")} />
|
||||
</Head>
|
||||
<div style={{ zIndex: 2, position: "relative" }}>
|
||||
{booking?.user?.organization?.calVideoLogo ? (
|
||||
<img
|
||||
className="h-5·w-auto fixed z-10 hidden sm:inline-block"
|
||||
src={`${WEBSITE_URL}/cal-logo-word-dark.svg`}
|
||||
alt="Cal.com Logo"
|
||||
className="min-w-16 min-h-16 fixed z-10 hidden aspect-square h-16 w-16 rounded-full sm:inline-block"
|
||||
src={booking.user.organization.calVideoLogo}
|
||||
alt="My Org Logo"
|
||||
style={{
|
||||
top: 46,
|
||||
left: 24,
|
||||
top: 32,
|
||||
left: 32,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className="fixed z-10 hidden sm:inline-block"
|
||||
src={`${WEBSITE_URL}/cal-logo-word-dark.svg`}
|
||||
alt="Logo"
|
||||
style={{
|
||||
top: 32,
|
||||
left: 32,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<VideoMeetingInfo booking={booking} />
|
||||
</>
|
||||
|
@ -288,6 +272,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
timeZone: true,
|
||||
name: true,
|
||||
email: true,
|
||||
organization: {
|
||||
select: {
|
||||
calVideoLogo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
references: {
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("apps/ A/B tests", () => {
|
||||
test("should point to the /future/apps/installed/[category]", 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("/apps/installed/messaging");
|
||||
|
||||
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: "Messaging" });
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
|
||||
test("should point to the /future/apps/[slug]", 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("/apps/telegram");
|
||||
|
||||
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: "Telegram" });
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
|
||||
test("should point to the /future/apps/[slug]/setup", 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("/apps/apple-calendar/setup");
|
||||
|
||||
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: "Connect to Apple Server" });
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
|
||||
test("should point to the /future/apps/categories", 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("/apps/categories");
|
||||
|
||||
await page.waitForLoadState();
|
||||
|
||||
const dataNextJsRouter = await page.evaluate(() =>
|
||||
window.document.documentElement.getAttribute("data-nextjs-router")
|
||||
);
|
||||
|
||||
expect(dataNextJsRouter).toEqual("app");
|
||||
|
||||
const locator = page.getByTestId("app-store-category-messaging");
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
|
||||
test("should point to the /future/apps/categories/[category]", 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("/apps/categories/messaging");
|
||||
|
||||
await page.waitForLoadState();
|
||||
|
||||
const dataNextJsRouter = await page.evaluate(() =>
|
||||
window.document.documentElement.getAttribute("data-nextjs-router")
|
||||
);
|
||||
|
||||
expect(dataNextJsRouter).toEqual("app");
|
||||
|
||||
const locator = page.getByText(/messaging apps/i);
|
||||
|
||||
await expect(locator).toBeVisible();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,418 @@
|
|||
/**
|
||||
* These e2e tests only aim to cover standard cases
|
||||
* Edge cases are currently handled in integration tests only
|
||||
*/
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { intervalLimitKeyToUnit } from "@calcom/lib/intervalLimit";
|
||||
import { entries } from "@calcom/prisma/zod-utils";
|
||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { bookTimeSlot, createUserWithLimits } from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
test.afterEach(async ({ users }) => {
|
||||
await users.deleteAll();
|
||||
});
|
||||
|
||||
// used as a multiplier for duration limits
|
||||
const EVENT_LENGTH = 30;
|
||||
|
||||
// limits used when testing each limit seperately
|
||||
const BOOKING_LIMITS_SINGLE = {
|
||||
PER_DAY: 2,
|
||||
PER_WEEK: 2,
|
||||
PER_MONTH: 2,
|
||||
PER_YEAR: 2,
|
||||
};
|
||||
|
||||
// limits used when testing multiple limits together
|
||||
const BOOKING_LIMITS_MULTIPLE = {
|
||||
PER_DAY: 1,
|
||||
PER_WEEK: 2,
|
||||
PER_MONTH: 3,
|
||||
PER_YEAR: 4,
|
||||
};
|
||||
|
||||
// prevent tests from crossing year boundaries - if currently in Oct or later, start booking in Jan instead of Nov
|
||||
// (we increment months twice when checking multiple limits)
|
||||
const firstDayInBookingMonth =
|
||||
dayjs().month() >= 9 ? dayjs().add(1, "year").month(0).date(1) : dayjs().add(1, "month").date(1);
|
||||
|
||||
// avoid weekly edge cases
|
||||
const firstMondayInBookingMonth = firstDayInBookingMonth.day(
|
||||
firstDayInBookingMonth.date() === firstDayInBookingMonth.startOf("week").date() ? 1 : 8
|
||||
);
|
||||
|
||||
// ensure we land on the same weekday when incrementing month
|
||||
const incrementDate = (date: Dayjs, unit: dayjs.ManipulateType) => {
|
||||
if (unit !== "month") return date.add(1, unit);
|
||||
return date.add(1, "month").day(date.day());
|
||||
};
|
||||
|
||||
const getLastEventUrlWithMonth = (user: Awaited<ReturnType<typeof createUserWithLimits>>, date: Dayjs) => {
|
||||
return `/${user.username}/${user.eventTypes.at(-1)?.slug}?month=${date.format("YYYY-MM")}`;
|
||||
};
|
||||
|
||||
test.describe("Booking limits", () => {
|
||||
entries(BOOKING_LIMITS_SINGLE).forEach(([limitKey, bookingLimit]) => {
|
||||
const limitUnit = intervalLimitKeyToUnit(limitKey);
|
||||
|
||||
// test one limit at a time
|
||||
test(limitUnit, async ({ page, users }) => {
|
||||
const slug = `booking-limit-${limitUnit}`;
|
||||
const singleLimit = { [limitKey]: bookingLimit };
|
||||
|
||||
const user = await createUserWithLimits({
|
||||
users,
|
||||
slug,
|
||||
length: EVENT_LENGTH,
|
||||
bookingLimits: singleLimit,
|
||||
});
|
||||
|
||||
let slotUrl = "";
|
||||
|
||||
const monthUrl = getLastEventUrlWithMonth(user, firstMondayInBookingMonth);
|
||||
await page.goto(monthUrl);
|
||||
|
||||
const availableDays = page.locator('[data-testid="day"][data-disabled="false"]');
|
||||
const bookingDay = availableDays.getByText(firstMondayInBookingMonth.date().toString(), {
|
||||
exact: true,
|
||||
});
|
||||
|
||||
// finish rendering days before counting
|
||||
await expect(bookingDay).toBeVisible({ timeout: 10_000 });
|
||||
const availableDaysBefore = await availableDays.count();
|
||||
|
||||
await test.step("can book up to limit", async () => {
|
||||
for (let i = 0; i < bookingLimit; i++) {
|
||||
await bookingDay.click();
|
||||
|
||||
await page.getByTestId("time").nth(0).click();
|
||||
await bookTimeSlot(page);
|
||||
|
||||
slotUrl = page.url();
|
||||
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
|
||||
await page.goto(monthUrl);
|
||||
}
|
||||
});
|
||||
|
||||
const expectedAvailableDays = {
|
||||
day: -1,
|
||||
week: -5,
|
||||
month: 0,
|
||||
year: 0,
|
||||
};
|
||||
|
||||
await test.step("but not over", async () => {
|
||||
// should already have navigated to monthUrl - just ensure days are rendered
|
||||
await expect(page.getByTestId("day").nth(0)).toBeVisible();
|
||||
|
||||
// ensure the day we just booked is now blocked
|
||||
await expect(bookingDay).toBeHidden({ timeout: 10_000 });
|
||||
|
||||
const availableDaysAfter = await availableDays.count();
|
||||
|
||||
// equals 0 if no available days, otherwise signed difference
|
||||
expect(availableDaysAfter && availableDaysAfter - availableDaysBefore).toBe(
|
||||
expectedAvailableDays[limitUnit]
|
||||
);
|
||||
|
||||
// try to book directly via form page
|
||||
await page.goto(slotUrl);
|
||||
await bookTimeSlot(page);
|
||||
|
||||
await expect(page.getByTestId("booking-fail")).toBeVisible({ timeout: 1000 });
|
||||
});
|
||||
|
||||
await test.step(`month after booking`, async () => {
|
||||
await page.goto(getLastEventUrlWithMonth(user, firstMondayInBookingMonth.add(1, "month")));
|
||||
|
||||
// finish rendering days before counting
|
||||
await expect(page.getByTestId("day").nth(0)).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// the month after we made bookings should have availability unless we hit a yearly limit
|
||||
await expect((await availableDays.count()) === 0).toBe(limitUnit === "year");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("multiple", async ({ page, users }) => {
|
||||
const slug = "booking-limit-multiple";
|
||||
|
||||
const user = await createUserWithLimits({
|
||||
users,
|
||||
slug,
|
||||
length: EVENT_LENGTH,
|
||||
bookingLimits: BOOKING_LIMITS_MULTIPLE,
|
||||
});
|
||||
|
||||
let slotUrl = "";
|
||||
|
||||
let bookingDate = firstMondayInBookingMonth;
|
||||
|
||||
// keep track of total bookings across multiple limits
|
||||
let bookingCount = 0;
|
||||
|
||||
for (const [limitKey, limitValue] of entries(BOOKING_LIMITS_MULTIPLE)) {
|
||||
const limitUnit = intervalLimitKeyToUnit(limitKey);
|
||||
|
||||
const monthUrl = getLastEventUrlWithMonth(user, bookingDate);
|
||||
await page.goto(monthUrl);
|
||||
|
||||
const availableDays = page.locator('[data-testid="day"][data-disabled="false"]');
|
||||
const bookingDay = availableDays.getByText(bookingDate.date().toString(), { exact: true });
|
||||
|
||||
// finish rendering days before counting
|
||||
await expect(bookingDay).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
const availableDaysBefore = await availableDays.count();
|
||||
|
||||
await test.step(`can book up ${limitUnit} to limit`, async () => {
|
||||
for (let i = 0; i + bookingCount < limitValue; i++) {
|
||||
await bookingDay.click();
|
||||
|
||||
await page.getByTestId("time").nth(0).click();
|
||||
await bookTimeSlot(page);
|
||||
bookingCount++;
|
||||
|
||||
slotUrl = page.url();
|
||||
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
|
||||
await page.goto(monthUrl);
|
||||
}
|
||||
});
|
||||
|
||||
const expectedAvailableDays = {
|
||||
day: -1,
|
||||
week: -4, // one day will already be blocked by daily limit
|
||||
month: 0,
|
||||
year: 0,
|
||||
};
|
||||
|
||||
await test.step("but not over", async () => {
|
||||
// should already have navigated to monthUrl - just ensure days are rendered
|
||||
await expect(page.getByTestId("day").nth(0)).toBeVisible();
|
||||
|
||||
// ensure the day we just booked is now blocked
|
||||
await expect(bookingDay).toBeHidden({ timeout: 10_000 });
|
||||
|
||||
const availableDaysAfter = await availableDays.count();
|
||||
|
||||
// equals 0 if no available days, otherwise signed difference
|
||||
expect(availableDaysAfter && availableDaysAfter - availableDaysBefore).toBe(
|
||||
expectedAvailableDays[limitUnit]
|
||||
);
|
||||
|
||||
// try to book directly via form page
|
||||
await page.goto(slotUrl);
|
||||
await bookTimeSlot(page);
|
||||
|
||||
await expect(page.getByTestId("booking-fail")).toBeVisible({ timeout: 1000 });
|
||||
});
|
||||
|
||||
await test.step(`month after booking`, async () => {
|
||||
await page.goto(getLastEventUrlWithMonth(user, bookingDate.add(1, "month")));
|
||||
|
||||
// finish rendering days before counting
|
||||
await expect(page.getByTestId("day").nth(0)).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// the month after we made bookings should have availability unless we hit a yearly limit
|
||||
await expect((await availableDays.count()) === 0).toBe(limitUnit === "year");
|
||||
});
|
||||
|
||||
// increment date by unit after hitting each limit
|
||||
bookingDate = incrementDate(bookingDate, limitUnit);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Duration limits", () => {
|
||||
entries(BOOKING_LIMITS_SINGLE).forEach(([limitKey, bookingLimit]) => {
|
||||
const limitUnit = intervalLimitKeyToUnit(limitKey);
|
||||
|
||||
// test one limit at a time
|
||||
test(limitUnit, async ({ page, users }) => {
|
||||
const slug = `duration-limit-${limitUnit}`;
|
||||
const singleLimit = { [limitKey]: bookingLimit * EVENT_LENGTH };
|
||||
|
||||
const user = await createUserWithLimits({
|
||||
users,
|
||||
slug,
|
||||
length: EVENT_LENGTH,
|
||||
durationLimits: singleLimit,
|
||||
});
|
||||
|
||||
let slotUrl = "";
|
||||
|
||||
const monthUrl = getLastEventUrlWithMonth(user, firstMondayInBookingMonth);
|
||||
await page.goto(monthUrl);
|
||||
|
||||
const availableDays = page.locator('[data-testid="day"][data-disabled="false"]');
|
||||
const bookingDay = availableDays.getByText(firstMondayInBookingMonth.date().toString(), {
|
||||
exact: true,
|
||||
});
|
||||
|
||||
// finish rendering days before counting
|
||||
await expect(bookingDay).toBeVisible({ timeout: 10_000 });
|
||||
const availableDaysBefore = await availableDays.count();
|
||||
|
||||
await test.step("can book up to limit", async () => {
|
||||
for (let i = 0; i < bookingLimit; i++) {
|
||||
await bookingDay.click();
|
||||
|
||||
await page.getByTestId("time").nth(0).click();
|
||||
await bookTimeSlot(page);
|
||||
|
||||
slotUrl = page.url();
|
||||
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
|
||||
await page.goto(monthUrl);
|
||||
}
|
||||
});
|
||||
|
||||
const expectedAvailableDays = {
|
||||
day: -1,
|
||||
week: -5,
|
||||
month: 0,
|
||||
year: 0,
|
||||
};
|
||||
|
||||
await test.step("but not over", async () => {
|
||||
// should already have navigated to monthUrl - just ensure days are rendered
|
||||
await expect(page.getByTestId("day").nth(0)).toBeVisible();
|
||||
|
||||
// ensure the day we just booked is now blocked
|
||||
await expect(bookingDay).toBeHidden({ timeout: 10_000 });
|
||||
|
||||
const availableDaysAfter = await availableDays.count();
|
||||
|
||||
// equals 0 if no available days, otherwise signed difference
|
||||
expect(availableDaysAfter && availableDaysAfter - availableDaysBefore).toBe(
|
||||
expectedAvailableDays[limitUnit]
|
||||
);
|
||||
|
||||
// try to book directly via form page
|
||||
await page.goto(slotUrl);
|
||||
await bookTimeSlot(page);
|
||||
|
||||
await expect(page.getByTestId("booking-fail")).toBeVisible({ timeout: 1000 });
|
||||
});
|
||||
|
||||
await test.step(`month after booking`, async () => {
|
||||
await page.goto(getLastEventUrlWithMonth(user, firstMondayInBookingMonth.add(1, "month")));
|
||||
|
||||
// finish rendering days before counting
|
||||
await expect(page.getByTestId("day").nth(0)).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// the month after we made bookings should have availability unless we hit a yearly limit
|
||||
await expect((await availableDays.count()) === 0).toBe(limitUnit === "year");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("multiple", async ({ page, users }) => {
|
||||
const slug = "duration-limit-multiple";
|
||||
|
||||
// multiply all booking limits by EVENT_LENGTH
|
||||
const durationLimits = entries(BOOKING_LIMITS_MULTIPLE).reduce((limits, [limitKey, bookingLimit]) => {
|
||||
return {
|
||||
...limits,
|
||||
[limitKey]: bookingLimit * EVENT_LENGTH,
|
||||
};
|
||||
}, {} as Record<keyof IntervalLimit, number>);
|
||||
|
||||
const user = await createUserWithLimits({
|
||||
users,
|
||||
slug,
|
||||
length: EVENT_LENGTH,
|
||||
durationLimits,
|
||||
});
|
||||
|
||||
let slotUrl = "";
|
||||
|
||||
let bookingDate = firstMondayInBookingMonth;
|
||||
|
||||
// keep track of total bookings across multiple limits
|
||||
let bookingCount = 0;
|
||||
|
||||
for (const [limitKey, limitValue] of entries(BOOKING_LIMITS_MULTIPLE)) {
|
||||
const limitUnit = intervalLimitKeyToUnit(limitKey);
|
||||
|
||||
const monthUrl = getLastEventUrlWithMonth(user, bookingDate);
|
||||
await page.goto(monthUrl);
|
||||
|
||||
const availableDays = page.locator('[data-testid="day"][data-disabled="false"]');
|
||||
const bookingDay = availableDays.getByText(bookingDate.date().toString(), { exact: true });
|
||||
|
||||
// finish rendering days before counting
|
||||
await expect(bookingDay).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
const availableDaysBefore = await availableDays.count();
|
||||
|
||||
await test.step(`can book up ${limitUnit} to limit`, async () => {
|
||||
for (let i = 0; i + bookingCount < limitValue; i++) {
|
||||
await bookingDay.click();
|
||||
|
||||
await page.getByTestId("time").nth(0).click();
|
||||
await bookTimeSlot(page);
|
||||
bookingCount++;
|
||||
|
||||
slotUrl = page.url();
|
||||
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
|
||||
await page.goto(monthUrl);
|
||||
}
|
||||
});
|
||||
|
||||
const expectedAvailableDays = {
|
||||
day: -1,
|
||||
week: -4, // one day will already be blocked by daily limit
|
||||
month: 0,
|
||||
year: 0,
|
||||
};
|
||||
|
||||
await test.step("but not over", async () => {
|
||||
// should already have navigated to monthUrl - just ensure days are rendered
|
||||
await expect(page.getByTestId("day").nth(0)).toBeVisible();
|
||||
|
||||
// ensure the day we just booked is now blocked
|
||||
await expect(bookingDay).toBeHidden({ timeout: 10_000 });
|
||||
|
||||
const availableDaysAfter = await availableDays.count();
|
||||
|
||||
// equals 0 if no available days, otherwise signed difference
|
||||
expect(availableDaysAfter && availableDaysAfter - availableDaysBefore).toBe(
|
||||
expectedAvailableDays[limitUnit]
|
||||
);
|
||||
|
||||
// try to book directly via form page
|
||||
await page.goto(slotUrl);
|
||||
await bookTimeSlot(page);
|
||||
|
||||
await expect(page.getByTestId("booking-fail")).toBeVisible({ timeout: 1000 });
|
||||
});
|
||||
|
||||
await test.step(`month after booking`, async () => {
|
||||
await page.goto(getLastEventUrlWithMonth(user, bookingDate.add(1, "month")));
|
||||
|
||||
// finish rendering days before counting
|
||||
await expect(page.getByTestId("day").nth(0)).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// the month after we made bookings should have availability unless we hit a yearly limit
|
||||
await expect((await availableDays.count()) === 0).toBe(limitUnit === "year");
|
||||
});
|
||||
|
||||
// increment date by unit after hitting each limit
|
||||
bookingDate = incrementDate(bookingDate, limitUnit);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -673,8 +673,8 @@ export async function login(
|
|||
await passwordLocator.fill(user.password ?? user.username!);
|
||||
await signInLocator.click();
|
||||
|
||||
// Moving away from waiting 2 seconds, as it is not a reliable way to expect session to be started
|
||||
await page.waitForLoadState("networkidle");
|
||||
// waiting for specific login request to resolve
|
||||
await page.waitForResponse(/\/api\/auth\/callback\/credentials/);
|
||||
}
|
||||
|
||||
export async function apiLogin(
|
||||
|
|
|
@ -11,6 +11,7 @@ import { totp } from "otplib";
|
|||
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||
|
||||
import type { Fixtures } from "./fixtures";
|
||||
import { test } from "./fixtures";
|
||||
|
@ -246,6 +247,38 @@ export async function expectEmailsToHaveSubject({
|
|||
expect(bookerFirstEmail.subject).toBe(emailSubject);
|
||||
}
|
||||
|
||||
export const createUserWithLimits = ({
|
||||
users,
|
||||
slug,
|
||||
title,
|
||||
length,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
}: {
|
||||
users: Fixtures["users"];
|
||||
slug: string;
|
||||
title?: string;
|
||||
length?: number;
|
||||
bookingLimits?: IntervalLimit;
|
||||
durationLimits?: IntervalLimit;
|
||||
}) => {
|
||||
if (!bookingLimits && !durationLimits) {
|
||||
throw new Error("Need to supply at least one of bookingLimits or durationLimits");
|
||||
}
|
||||
|
||||
return users.create({
|
||||
eventTypes: [
|
||||
{
|
||||
slug,
|
||||
title: title ?? slug,
|
||||
length: length ?? 30,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
// this method is not used anywhere else
|
||||
// but I'm keeping it here in case we need in the future
|
||||
async function createUserWithSeatedEvent(users: Fixtures["users"]) {
|
||||
|
|
|
@ -11,7 +11,8 @@ test.describe("unauthorized user sees correct translations (de)", async () => {
|
|||
|
||||
test("should use correct translations and html attributes", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("load");
|
||||
// we dont need to wait for styles and images, only for dom
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=de]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
@ -35,7 +36,7 @@ test.describe("unauthorized user sees correct translations (ar)", async () => {
|
|||
|
||||
test("should use correct translations and html attributes", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("load");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=ar]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=rtl]").waitFor({ state: "attached" });
|
||||
|
@ -59,7 +60,7 @@ test.describe("unauthorized user sees correct translations (zh)", async () => {
|
|||
|
||||
test("should use correct translations and html attributes", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("load");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=zh]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
@ -83,7 +84,7 @@ test.describe("unauthorized user sees correct translations (zh-CN)", async () =>
|
|||
|
||||
test("should use correct translations and html attributes", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("load");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=zh-CN]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
@ -107,7 +108,7 @@ test.describe("unauthorized user sees correct translations (zh-TW)", async () =>
|
|||
|
||||
test("should use correct translations and html attributes", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("load");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=zh-TW]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
@ -131,7 +132,7 @@ test.describe("unauthorized user sees correct translations (pt)", async () => {
|
|||
|
||||
test("should use correct translations and html attributes", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("load");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=pt]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
@ -155,7 +156,7 @@ test.describe("unauthorized user sees correct translations (pt-br)", async () =>
|
|||
|
||||
test("should use correct translations and html attributes", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("load");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=pt-BR]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
@ -179,7 +180,7 @@ test.describe("unauthorized user sees correct translations (es-419)", async () =
|
|||
|
||||
test("should use correct translations and html attributes", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("load");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
// es-419 is disabled in i18n config, so es should be used as fallback
|
||||
await page.locator("html[lang=es]").waitFor({ state: "attached" });
|
||||
|
@ -213,57 +214,61 @@ test.describe("authorized user sees correct translations (de)", async () => {
|
|||
await test.step("should navigate to /event-types and show German translations", async () => {
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=de]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
const locator = page.getByText("Ereignistypen", { exact: true });
|
||||
expect(await locator.count()).toBeGreaterThanOrEqual(1);
|
||||
const locator = page.getByRole("heading", { name: "Ereignistypen", exact: true });
|
||||
// locator.count() does not wait for elements
|
||||
// but event-types page is client side, so it takes some time to render html
|
||||
// thats why we need to use method that awaits for the element
|
||||
// https://github.com/microsoft/playwright/issues/14278#issuecomment-1131754679
|
||||
await expect(locator).toHaveCount(1);
|
||||
}
|
||||
|
||||
{
|
||||
const locator = page.getByText("Event Types", { exact: true });
|
||||
expect(await locator.count()).toEqual(0);
|
||||
await expect(locator).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step("should navigate to /bookings and show German translations", async () => {
|
||||
await page.goto("/bookings");
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=de]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
const locator = page.getByText("Buchungen", { exact: true });
|
||||
expect(await locator.count()).toBeGreaterThanOrEqual(1);
|
||||
const locator = page.getByRole("heading", { name: "Buchungen", exact: true });
|
||||
await expect(locator).toHaveCount(1);
|
||||
}
|
||||
|
||||
{
|
||||
const locator = page.getByText("Bookings", { exact: true });
|
||||
expect(await locator.count()).toEqual(0);
|
||||
await expect(locator).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step("should reload the /bookings and show German translations", async () => {
|
||||
await page.reload();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=de]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
const locator = page.getByText("Buchungen", { exact: true });
|
||||
expect(await locator.count()).toBeGreaterThanOrEqual(1);
|
||||
const locator = page.getByRole("heading", { name: "Buchungen", exact: true });
|
||||
await expect(locator).toHaveCount(1);
|
||||
}
|
||||
|
||||
{
|
||||
const locator = page.getByText("Bookings", { exact: true });
|
||||
expect(await locator.count()).toEqual(0);
|
||||
await expect(locator).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -285,57 +290,57 @@ test.describe("authorized user sees correct translations (pt-br)", async () => {
|
|||
await test.step("should navigate to /event-types and show Brazil-Portuguese translations", async () => {
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=pt-br]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
const locator = page.getByText("Tipos de Eventos", { exact: true });
|
||||
expect(await locator.count()).toBeGreaterThanOrEqual(1);
|
||||
const locator = page.getByRole("heading", { name: "Tipos de Eventos", exact: true });
|
||||
await expect(locator).toHaveCount(1);
|
||||
}
|
||||
|
||||
{
|
||||
const locator = page.getByText("Event Types", { exact: true });
|
||||
expect(await locator.count()).toEqual(0);
|
||||
await expect(locator).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step("should navigate to /bookings and show Brazil-Portuguese translations", async () => {
|
||||
await page.goto("/bookings");
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=pt-br]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
const locator = page.getByText("Reservas", { exact: true });
|
||||
expect(await locator.count()).toBeGreaterThanOrEqual(1);
|
||||
const locator = page.getByRole("heading", { name: "Reservas", exact: true });
|
||||
await expect(locator).toHaveCount(1);
|
||||
}
|
||||
|
||||
{
|
||||
const locator = page.getByText("Bookings", { exact: true });
|
||||
expect(await locator.count()).toEqual(0);
|
||||
await expect(locator).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step("should reload the /bookings and show Brazil-Portuguese translations", async () => {
|
||||
await page.reload();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=pt-br]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
const locator = page.getByText("Reservas", { exact: true });
|
||||
expect(await locator.count()).toBeGreaterThanOrEqual(1);
|
||||
const locator = page.getByRole("heading", { name: "Reservas", exact: true });
|
||||
await expect(locator).toHaveCount(1);
|
||||
}
|
||||
|
||||
{
|
||||
const locator = page.getByText("Bookings", { exact: true });
|
||||
expect(await locator.count()).toEqual(0);
|
||||
await expect(locator).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -357,57 +362,57 @@ test.describe("authorized user sees correct translations (ar)", async () => {
|
|||
await test.step("should navigate to /event-types and show Arabic translations", async () => {
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=ar]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=rtl]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
const locator = page.getByText("أنواع الحدث", { exact: true });
|
||||
expect(await locator.count()).toBeGreaterThanOrEqual(1);
|
||||
const locator = page.getByRole("heading", { name: "أنواع الحدث", exact: true });
|
||||
await expect(locator).toHaveCount(1);
|
||||
}
|
||||
|
||||
{
|
||||
const locator = page.getByText("Event Types", { exact: true });
|
||||
expect(await locator.count()).toEqual(0);
|
||||
await expect(locator).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step("should navigate to /bookings and show Arabic translations", async () => {
|
||||
await page.goto("/bookings");
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=ar]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=rtl]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
const locator = page.getByText("عمليات الحجز", { exact: true });
|
||||
expect(await locator.count()).toBeGreaterThanOrEqual(1);
|
||||
const locator = page.getByRole("heading", { name: "عمليات الحجز", exact: true });
|
||||
await expect(locator).toHaveCount(1);
|
||||
}
|
||||
|
||||
{
|
||||
const locator = page.getByText("Bookings", { exact: true });
|
||||
expect(await locator.count()).toEqual(0);
|
||||
await expect(locator).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step("should reload the /bookings and show Arabic translations", async () => {
|
||||
await page.reload();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=ar]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=rtl]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
const locator = page.getByText("عمليات الحجز", { exact: true });
|
||||
expect(await locator.count()).toBeGreaterThanOrEqual(1);
|
||||
const locator = page.getByRole("heading", { name: "عمليات الحجز", exact: true });
|
||||
await expect(locator).toHaveCount(1);
|
||||
}
|
||||
|
||||
{
|
||||
const locator = page.getByText("Bookings", { exact: true });
|
||||
expect(await locator.count()).toEqual(0);
|
||||
await expect(locator).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -429,7 +434,7 @@ test.describe("authorized user sees changed translations (de->ar)", async () =>
|
|||
await test.step("should change the language and show Arabic translations", async () => {
|
||||
await page.goto("/settings/my-account/general");
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator(".bg-default > div > div:nth-child(2)").first().click();
|
||||
await page.locator("#react-select-2-option-0").click();
|
||||
|
@ -444,32 +449,33 @@ test.describe("authorized user sees changed translations (de->ar)", async () =>
|
|||
await page.locator("html[dir=rtl]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
const locator = page.getByText("عام", { exact: true }); // "general"
|
||||
expect(await locator.count()).toBeGreaterThanOrEqual(1);
|
||||
// at least one is visible
|
||||
const locator = page.getByText("عام", { exact: true }).last(); // "general"
|
||||
await expect(locator).toBeVisible();
|
||||
}
|
||||
|
||||
{
|
||||
const locator = page.getByText("Allgemein", { exact: true }); // "general"
|
||||
expect(await locator.count()).toEqual(0);
|
||||
await expect(locator).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step("should reload and show Arabic translations", async () => {
|
||||
await page.reload();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=ar]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=rtl]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
const locator = page.getByText("عام", { exact: true }); // "general"
|
||||
expect(await locator.count()).toBeGreaterThanOrEqual(1);
|
||||
const locator = page.getByText("عام", { exact: true }).last(); // "general"
|
||||
await expect(locator).toBeVisible();
|
||||
}
|
||||
|
||||
{
|
||||
const locator = page.getByText("Allgemein", { exact: true }); // "general"
|
||||
expect(await locator.count()).toEqual(0);
|
||||
await expect(locator).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -491,7 +497,7 @@ test.describe("authorized user sees changed translations (de->pt-BR) [locale1]",
|
|||
await test.step("should change the language and show Brazil-Portuguese translations", async () => {
|
||||
await page.goto("/settings/my-account/general");
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator(".bg-default > div > div:nth-child(2)").first().click();
|
||||
await page.locator("#react-select-2-option-14").click();
|
||||
|
@ -506,32 +512,32 @@ test.describe("authorized user sees changed translations (de->pt-BR) [locale1]",
|
|||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
const locator = page.getByText("Geral", { exact: true }); // "general"
|
||||
expect(await locator.count()).toBeGreaterThanOrEqual(1);
|
||||
const locator = page.getByText("Geral", { exact: true }).last(); // "general"
|
||||
await expect(locator).toBeVisible();
|
||||
}
|
||||
|
||||
{
|
||||
const locator = page.getByText("Allgemein", { exact: true }); // "general"
|
||||
expect(await locator.count()).toEqual(0);
|
||||
await expect(locator).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
|
||||
await test.step("should reload and show Brazil-Portuguese translations", async () => {
|
||||
await page.reload();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
|
||||
await page.locator("html[lang=pt-BR]").waitFor({ state: "attached" });
|
||||
await page.locator("html[dir=ltr]").waitFor({ state: "attached" });
|
||||
|
||||
{
|
||||
const locator = page.getByText("Geral", { exact: true }); // "general"
|
||||
expect(await locator.count()).toBeGreaterThanOrEqual(1);
|
||||
const locator = page.getByText("Geral", { exact: true }).last(); // "general"
|
||||
await expect(locator).toBeVisible();
|
||||
}
|
||||
|
||||
{
|
||||
const locator = page.getByText("Allgemein", { exact: true }); // "general"
|
||||
expect(await locator.count()).toEqual(0);
|
||||
await expect(locator).toHaveCount(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -54,7 +54,6 @@ test.describe("Organization", () => {
|
|||
// 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(
|
||||
|
|
|
@ -4,7 +4,8 @@ import { randomBytes } from "crypto";
|
|||
import { APP_NAME, IS_PREMIUM_USERNAME_ENABLED, IS_MAILHOG_ENABLED } from "@calcom/lib/constants";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { getEmailsReceivedByUser } from "./lib/testUtils";
|
||||
import { getEmailsReceivedByUser, localize } from "./lib/testUtils";
|
||||
import { expectInvitationEmailToBeReceived } from "./team/expects";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
|
@ -12,8 +13,9 @@ test.describe("Signup Flow Test", async () => {
|
|||
test.beforeEach(async ({ features }) => {
|
||||
features.reset(); // This resets to the inital state not an empt yarray
|
||||
});
|
||||
test.afterAll(async ({ users }) => {
|
||||
test.afterAll(async ({ users, emails }) => {
|
||||
await users.deleteAll();
|
||||
emails?.deleteAll();
|
||||
});
|
||||
test("Username is taken", async ({ page, users }) => {
|
||||
// log in trail user
|
||||
|
@ -228,4 +230,55 @@ test.describe("Signup Flow Test", async () => {
|
|||
const verifyEmail = receivedEmails?.items[0];
|
||||
expect(verifyEmail?.subject).toBe(`${APP_NAME}: Verify your account`);
|
||||
});
|
||||
test("If signup is disabled allow team invites", async ({ browser, page, users, emails }) => {
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== "true", "Skipping due to signup being enabled");
|
||||
|
||||
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("Invite User to team", async () => {
|
||||
// TODO: This invite logic should live in a fixture - its used in team and orgs invites (Duplicated from team/org invites)
|
||||
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;
|
||||
|
||||
// Follow invite link to new window
|
||||
const context = await browser.newContext();
|
||||
const newPage = await context.newPage();
|
||||
await newPage.goto(inviteLink);
|
||||
await newPage.waitForLoadState("networkidle");
|
||||
|
||||
const url = new URL(newPage.url());
|
||||
expect(url.pathname).toBe("/signup");
|
||||
|
||||
// Check required fields
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -418,6 +418,45 @@ test.describe("Teams - Org", () => {
|
|||
expect(teamMatesObj.concat([{ name: owner.name! }]).some(({ name }) => name === chosenUser)).toBe(true);
|
||||
// TODO: Assert whether the user received an email
|
||||
});
|
||||
|
||||
test("Can access booking page with event slug and team page in lowercase/uppercase/mixedcase", async ({
|
||||
page,
|
||||
orgs,
|
||||
users,
|
||||
}) => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
});
|
||||
const teamMatesObj = [
|
||||
{ name: "teammate-1" },
|
||||
{ name: "teammate-2" },
|
||||
{ name: "teammate-3" },
|
||||
{ name: "teammate-4" },
|
||||
];
|
||||
|
||||
const owner = await users.create(
|
||||
{
|
||||
username: "pro-user",
|
||||
name: "pro-user",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
},
|
||||
{
|
||||
hasTeam: true,
|
||||
teammates: teamMatesObj,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
}
|
||||
);
|
||||
const { team } = await owner.getFirstTeam();
|
||||
const { slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||
|
||||
const teamSlugUpperCase = team.slug?.toUpperCase();
|
||||
const teamEventSlugUpperCase = teamEventSlug.toUpperCase();
|
||||
|
||||
// This is the most closest to the actual user flow as org1.cal.com maps to /org/orgSlug
|
||||
await page.goto(`/org/${org.slug}/${teamSlugUpperCase}/${teamEventSlugUpperCase}`);
|
||||
await page.waitForSelector("[data-testid=day]");
|
||||
});
|
||||
});
|
||||
|
||||
async function doOnOrgDomain(
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "اختيار الوقت",
|
||||
"select_date": "اختيار التاريخ",
|
||||
"see_all_available_times": "رؤية كل الأوقات المتاحة",
|
||||
"org_team_names_example": "مثال، فريق التسويق",
|
||||
"org_team_names_example_1": "مثال، فريق التسويق",
|
||||
"org_team_names_example_2": "مثال، فريق المبيعات",
|
||||
"org_team_names_example_3": "مثال، فريق التصميم",
|
||||
"org_team_names_example_4": "مثال، الفريق الهندسي",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Vyberte čas",
|
||||
"select_date": "Vyberte datum",
|
||||
"see_all_available_times": "Zobrazit všechny dostupné časy",
|
||||
"org_team_names_example": "Např. marketingový tým",
|
||||
"org_team_names_example_1": "Např. marketingový tým",
|
||||
"org_team_names_example_2": "Např. obchodní tým",
|
||||
"org_team_names_example_3": "Např. designérský tým",
|
||||
"org_team_names_example_4": "Např. inženýrský tým",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Zeit auswählen",
|
||||
"select_date": "Datum auswählen",
|
||||
"see_all_available_times": "Alle verfügbaren Zeiten ansehen",
|
||||
"org_team_names_example": "z.B. Marketing-Team",
|
||||
"org_team_names_example_1": "z.B. Marketing-Team",
|
||||
"org_team_names_example_2": "z.B. Vertriebsteam",
|
||||
"org_team_names_example_3": "z. B. Design-Team",
|
||||
"org_team_names_example_4": "z.B. Engineering-Team",
|
||||
|
|
|
@ -880,6 +880,7 @@
|
|||
"toggle_calendars_conflict": "Toggle the calendars you want to check for conflicts to prevent double bookings.",
|
||||
"connect_additional_calendar": "Connect additional calendar",
|
||||
"calendar_updated_successfully": "Calendar updated successfully",
|
||||
"check_here":"Check here",
|
||||
"conferencing": "Conferencing",
|
||||
"calendar": "Calendar",
|
||||
"payments": "Payments",
|
||||
|
@ -1637,6 +1638,7 @@
|
|||
"individual": "Individual",
|
||||
"all_bookings_filter_label": "All Bookings",
|
||||
"all_users_filter_label": "All Users",
|
||||
"all_event_types_filter_label": "All Event Types",
|
||||
"your_bookings_filter_label": "Your Bookings",
|
||||
"meeting_url_variable": "Meeting url",
|
||||
"meeting_url_info": "The event meeting conference url",
|
||||
|
@ -1867,6 +1869,7 @@
|
|||
"review_event_type": "Review Event Type",
|
||||
"looking_for_more_analytics": "Looking for more analytics?",
|
||||
"looking_for_more_insights": "Looking for more Insights?",
|
||||
"filters": "Filters",
|
||||
"add_filter": "Add filter",
|
||||
"remove_filters": "Clear all filters",
|
||||
"select_user": "Select User",
|
||||
|
@ -2024,7 +2027,7 @@
|
|||
"select_time": "Select Time",
|
||||
"select_date": "Select Date",
|
||||
"see_all_available_times": "See all available times",
|
||||
"org_team_names_example": "e.g. Marketing Team",
|
||||
"org_team_names_example_1": "e.g. Marketing Team",
|
||||
"org_team_names_example_2": "e.g. Sales Team",
|
||||
"org_team_names_example_3": "e.g. Design Team",
|
||||
"org_team_names_example_4": "e.g. Engineering Team",
|
||||
|
@ -2040,6 +2043,9 @@
|
|||
"description_requires_booker_email_verification": "To ensure booker's email verification before scheduling events",
|
||||
"requires_confirmation_mandatory": "Text messages can only be sent to attendees when event type requires confirmation.",
|
||||
"organizations": "Organizations",
|
||||
"upload_cal_video_logo":"Upload Cal Video Logo",
|
||||
"update_cal_video_logo":"Update Cal Video Logo",
|
||||
"cal_video_logo_upload_instruction":"To ensure your logo is visible against Cal video's dark background, please upload a light-colored image in PNG or SVG format to maintain transparency.",
|
||||
"org_admin_other_teams": "Other teams",
|
||||
"org_admin_other_teams_description": "Here you can see teams inside your organization that you are not part of. You can add yourself to them if needed.",
|
||||
"no_other_teams_found": "No other teams found",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Seleccione la hora",
|
||||
"select_date": "Seleccione la fecha",
|
||||
"see_all_available_times": "Ver todas las horas disponibles",
|
||||
"org_team_names_example": "ej. Equipo de marketing",
|
||||
"org_team_names_example_1": "ej. Equipo de marketing",
|
||||
"org_team_names_example_2": "ej. Equipo de ventas",
|
||||
"org_team_names_example_3": "ej. Equipo de diseño",
|
||||
"org_team_names_example_4": "ej. Equipo de ingeniería",
|
||||
|
|
|
@ -2000,7 +2000,7 @@
|
|||
"select_time": "Sélectionner un créneau",
|
||||
"select_date": "Sélectionner une date",
|
||||
"see_all_available_times": "Voir tous les créneaux disponibles",
|
||||
"org_team_names_example": "p. ex. Équipe marketing",
|
||||
"org_team_names_example_1": "p. ex. Équipe marketing",
|
||||
"org_team_names_example_2": "p. ex. Équipe de vente",
|
||||
"org_team_names_example_3": "p. ex. Équipe de design",
|
||||
"org_team_names_example_4": "p. ex. Équipe d'ingénierie",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "בחירת שעה",
|
||||
"select_date": "בחירת תאריך",
|
||||
"see_all_available_times": "לצפייה בכל המועדים הפנויים",
|
||||
"org_team_names_example": "לדוגמה, מחלקת שיווק",
|
||||
"org_team_names_example_1": "לדוגמה, מחלקת שיווק",
|
||||
"org_team_names_example_2": "לדוגמה, מחלקת מכירות",
|
||||
"org_team_names_example_3": "לדוגמה, מחלקת עיצוב",
|
||||
"org_team_names_example_4": "לדוגמה, מחלקת הנדסה",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Seleziona l'ora",
|
||||
"select_date": "Seleziona la data",
|
||||
"see_all_available_times": "Vedi tutti gli orari disponibili",
|
||||
"org_team_names_example": "ad es., team di marketing",
|
||||
"org_team_names_example_1": "ad es., team di marketing",
|
||||
"org_team_names_example_2": "ad es., team vendite",
|
||||
"org_team_names_example_3": "ad es., team di progettazione",
|
||||
"org_team_names_example_4": "ad es., team di ingegneria",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "時間帯を選ぶ",
|
||||
"select_date": "日付を選ぶ",
|
||||
"see_all_available_times": "出席できる時間帯をすべて表示",
|
||||
"org_team_names_example": "例:マーケティングチーム",
|
||||
"org_team_names_example_1": "例:マーケティングチーム",
|
||||
"org_team_names_example_2": "例:営業チーム",
|
||||
"org_team_names_example_3": "例:デザインチーム",
|
||||
"org_team_names_example_4": "例:エンジニアリングチーム",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "시간 선택",
|
||||
"select_date": "날짜 선택",
|
||||
"see_all_available_times": "모든 사용 가능한 시간 보기",
|
||||
"org_team_names_example": "예: 마케팅 팀",
|
||||
"org_team_names_example_1": "예: 마케팅 팀",
|
||||
"org_team_names_example_2": "예: 세일즈 팀",
|
||||
"org_team_names_example_3": "예: 디자인 팀",
|
||||
"org_team_names_example_4": "예: 엔지니어링 팀",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Tijd selecteren",
|
||||
"select_date": "Datum selecteren",
|
||||
"see_all_available_times": "Bekijk alle beschikbare tijden",
|
||||
"org_team_names_example": "bijvoorbeeld Marketingteam",
|
||||
"org_team_names_example_1": "bijvoorbeeld Marketingteam",
|
||||
"org_team_names_example_2": "bijvoorbeeld Verkoopteam",
|
||||
"org_team_names_example_3": "bijvoorbeeld Ontwerpteam",
|
||||
"org_team_names_example_4": "bijvoorbeeld Engineeringteam",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Wybierz godzinę",
|
||||
"select_date": "Wybierz datę",
|
||||
"see_all_available_times": "Zobacz wszystkie dostępne godziny",
|
||||
"org_team_names_example": "np. zespół marketingowy",
|
||||
"org_team_names_example_1": "np. zespół marketingowy",
|
||||
"org_team_names_example_2": "np. zespół ds. sprzedaży",
|
||||
"org_team_names_example_3": "np. zespół projektowy",
|
||||
"org_team_names_example_4": "np. zespół ds. inżynierii",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Selecione o horário",
|
||||
"select_date": "Selecione a data",
|
||||
"see_all_available_times": "Veja todos os horários disponíveis",
|
||||
"org_team_names_example": "ex.: Time de Marketing",
|
||||
"org_team_names_example_1": "ex.: Time de Marketing",
|
||||
"org_team_names_example_2": "ex: Time de Vendas",
|
||||
"org_team_names_example_3": "ex: Time de Design",
|
||||
"org_team_names_example_4": "ex: Time de Engenharia",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Selecionar horário",
|
||||
"select_date": "Selecionar data",
|
||||
"see_all_available_times": "Ver todos os horários disponíveis",
|
||||
"org_team_names_example": "Por exemplo, Equipa de Marketing",
|
||||
"org_team_names_example_1": "Por exemplo, Equipa de Marketing",
|
||||
"org_team_names_example_2": "Por exemplo, Equipa de Vendas",
|
||||
"org_team_names_example_3": "Por exemplo, Equipa de Design",
|
||||
"org_team_names_example_4": "Por exemplo, Equipa de Engenharia",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Selectați ora",
|
||||
"select_date": "Selectați data",
|
||||
"see_all_available_times": "Vedeți toate orele disponibile",
|
||||
"org_team_names_example": "de ex. echipa de marketing",
|
||||
"org_team_names_example_1": "de ex. echipa de marketing",
|
||||
"org_team_names_example_2": "de ex. echipa de vânzări",
|
||||
"org_team_names_example_3": "de ex. echipa de design",
|
||||
"org_team_names_example_4": "de ex. echipa de inginerie",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Выбрать время",
|
||||
"select_date": "Выбрать дату",
|
||||
"see_all_available_times": "Посмотреть все доступные интервалы времени",
|
||||
"org_team_names_example": "например, команда по маркетингу",
|
||||
"org_team_names_example_1": "например, команда по маркетингу",
|
||||
"org_team_names_example_2": "например, Отдел продаж",
|
||||
"org_team_names_example_3": "например, отдел дизайна",
|
||||
"org_team_names_example_4": "например, технический отдел",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Izaberite vreme",
|
||||
"select_date": "Izaberite datum",
|
||||
"see_all_available_times": "Pogledajte sva dostupna vremena",
|
||||
"org_team_names_example": "npr. Marketing tim",
|
||||
"org_team_names_example_1": "npr. Marketing tim",
|
||||
"org_team_names_example_2": "npr. Prodajni tim",
|
||||
"org_team_names_example_3": "npr. Dizajnerski tim",
|
||||
"org_team_names_example_4": "npr. Inženjerski tim",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Välj tid",
|
||||
"select_date": "Välj datum",
|
||||
"see_all_available_times": "Se alla tillgängliga tider",
|
||||
"org_team_names_example": "t.ex. marknadsföringsteam",
|
||||
"org_team_names_example_1": "t.ex. marknadsföringsteam",
|
||||
"org_team_names_example_2": "t.ex. säljteam",
|
||||
"org_team_names_example_3": "t.ex. designteam",
|
||||
"org_team_names_example_4": "t.ex. ingenjörsteam",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Saati Seçin",
|
||||
"select_date": "Tarihi Seçin",
|
||||
"see_all_available_times": "Tüm müsait saatleri görün",
|
||||
"org_team_names_example": "Örneğin. Pazarlama ekibi",
|
||||
"org_team_names_example_1": "Örneğin. Pazarlama ekibi",
|
||||
"org_team_names_example_2": "Örn. Satış Ekibi",
|
||||
"org_team_names_example_3": "Örn. Tasarım Ekibi",
|
||||
"org_team_names_example_4": "Örn. Mühendislik Ekibi",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Виберіть час",
|
||||
"select_date": "Виберіть дату",
|
||||
"see_all_available_times": "Переглянути доступні часові проміжки",
|
||||
"org_team_names_example": "напр. команда маркетингу",
|
||||
"org_team_names_example_1": "напр. команда маркетингу",
|
||||
"org_team_names_example_2": "напр. відділ продажів",
|
||||
"org_team_names_example_3": "напр. дизайнерський відділ",
|
||||
"org_team_names_example_4": "e.g. інженерний відділ",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "Chọn thời gian",
|
||||
"select_date": "Chọn ngày",
|
||||
"see_all_available_times": "Xem tất cả những thời gian trống",
|
||||
"org_team_names_example": "ví dụ Nhóm Tiếp thị",
|
||||
"org_team_names_example_1": "ví dụ Nhóm Tiếp thị",
|
||||
"org_team_names_example_2": "ví dụ Nhóm Kinh doanh",
|
||||
"org_team_names_example_3": "ví dụ Nhóm Thiết kế",
|
||||
"org_team_names_example_4": "ví dụ Nhóm Kỹ thuật",
|
||||
|
|
|
@ -1995,7 +1995,7 @@
|
|||
"select_time": "选择时间",
|
||||
"select_date": "选择日期",
|
||||
"see_all_available_times": "查看所有可预约时间",
|
||||
"org_team_names_example": "例如,营销团队",
|
||||
"org_team_names_example_1": "例如,营销团队",
|
||||
"org_team_names_example_2": "例如,销售团队",
|
||||
"org_team_names_example_3": "例如,设计团队",
|
||||
"org_team_names_example_4": "例如,工程团队",
|
||||
|
|
|
@ -1994,7 +1994,7 @@
|
|||
"select_time": "選取時間",
|
||||
"select_date": "選取日期",
|
||||
"see_all_available_times": "查看所有可預約時段",
|
||||
"org_team_names_example": "例如行銷團隊",
|
||||
"org_team_names_example_1": "例如行銷團隊",
|
||||
"org_team_names_example_2": "例如銷售團隊",
|
||||
"org_team_names_example_3": "例如設計團隊",
|
||||
"org_team_names_example_4": "例如工程團隊",
|
||||
|
|
|
@ -12,6 +12,7 @@ import "vitest-fetch-mock";
|
|||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { handleStripePaymentSuccess } from "@calcom/features/ee/payments/api/webhook";
|
||||
import { weekdayToWeekIndex, type WeekDays } from "@calcom/lib/date-fns";
|
||||
import type { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
|
@ -19,7 +20,7 @@ import type { SchedulingType } from "@calcom/prisma/enums";
|
|||
import type { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
import type { NewCalendarEventType } from "@calcom/types/Calendar";
|
||||
import type { EventBusyDate } from "@calcom/types/Calendar";
|
||||
import type { EventBusyDate, IntervalLimit } from "@calcom/types/Calendar";
|
||||
|
||||
import { getMockPaymentService } from "./MockPaymentService";
|
||||
|
||||
|
@ -89,6 +90,7 @@ type InputUser = Omit<typeof TestData.users.example, "defaultScheduleId"> & {
|
|||
timeZone: string;
|
||||
}[];
|
||||
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
|
||||
weekStart?: string;
|
||||
};
|
||||
|
||||
export type InputEventType = {
|
||||
|
@ -110,10 +112,9 @@ export type InputEventType = {
|
|||
requiresConfirmation?: boolean;
|
||||
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
|
||||
schedule?: InputUser["schedules"][number];
|
||||
bookingLimits?: {
|
||||
PER_DAY?: number;
|
||||
};
|
||||
} & Partial<Omit<Prisma.EventTypeCreateInput, "users" | "schedule">>;
|
||||
bookingLimits?: IntervalLimit;
|
||||
durationLimits?: IntervalLimit;
|
||||
} & Partial<Omit<Prisma.EventTypeCreateInput, "users" | "schedule" | "bookingLimits" | "durationLimits">>;
|
||||
|
||||
type WhiteListedBookingProps = {
|
||||
id?: number;
|
||||
|
@ -536,20 +537,32 @@ export async function createOrganization(orgData: { name: string; slug: string }
|
|||
* - `dateIncrement` adds the increment to current day
|
||||
* - `monthIncrement` adds the increment to current month
|
||||
* - `yearIncrement` adds the increment to current year
|
||||
* - `fromDate` starts incrementing from this date (default: today)
|
||||
*/
|
||||
export const getDate = (
|
||||
param: { dateIncrement?: number; monthIncrement?: number; yearIncrement?: number } = {}
|
||||
param: {
|
||||
dateIncrement?: number;
|
||||
monthIncrement?: number;
|
||||
yearIncrement?: number;
|
||||
fromDate?: Date;
|
||||
} = {}
|
||||
) => {
|
||||
let { dateIncrement, monthIncrement, yearIncrement } = param;
|
||||
let { dateIncrement, monthIncrement, yearIncrement, fromDate } = param;
|
||||
|
||||
dateIncrement = dateIncrement || 0;
|
||||
monthIncrement = monthIncrement || 0;
|
||||
yearIncrement = yearIncrement || 0;
|
||||
fromDate = fromDate || new Date();
|
||||
|
||||
let _date = new Date().getDate() + dateIncrement;
|
||||
let year = new Date().getFullYear() + yearIncrement;
|
||||
fromDate.setDate(fromDate.getDate() + dateIncrement);
|
||||
fromDate.setMonth(fromDate.getMonth() + monthIncrement);
|
||||
fromDate.setFullYear(fromDate.getFullYear() + yearIncrement);
|
||||
|
||||
let _date = fromDate.getDate();
|
||||
let year = fromDate.getFullYear();
|
||||
|
||||
// Make it start with 1 to match with DayJS requiremet
|
||||
let _month = new Date().getMonth() + monthIncrement + 1;
|
||||
let _month = fromDate.getMonth() + 1;
|
||||
|
||||
// If last day of the month(As _month is plus 1 already it is going to be the 0th day of next month which is the last day of current month)
|
||||
const lastDayOfMonth = new Date(year, _month, 0).getDate();
|
||||
|
@ -568,13 +581,35 @@ export const getDate = (
|
|||
const month = _month < 10 ? `0${_month}` : _month;
|
||||
|
||||
return {
|
||||
date,
|
||||
month,
|
||||
year,
|
||||
date: String(date),
|
||||
month: String(month),
|
||||
year: String(year),
|
||||
dateString: `${year}-${month}-${date}`,
|
||||
};
|
||||
};
|
||||
|
||||
const isWeekStart = (date: Date, weekStart: WeekDays) => {
|
||||
return date.getDay() === weekdayToWeekIndex(weekStart);
|
||||
};
|
||||
|
||||
export const getNextMonthNotStartingOnWeekStart = (weekStart: WeekDays, from?: Date) => {
|
||||
const date = from ?? new Date();
|
||||
|
||||
const incrementMonth = (date: Date) => {
|
||||
date.setMonth(date.getMonth() + 1);
|
||||
};
|
||||
|
||||
// start searching from the 1st day of next month
|
||||
incrementMonth(date);
|
||||
date.setDate(1);
|
||||
|
||||
while (isWeekStart(date, weekStart)) {
|
||||
incrementMonth(date);
|
||||
}
|
||||
|
||||
return getDate({ fromDate: date });
|
||||
};
|
||||
|
||||
export function getMockedCredential({
|
||||
metadataLookupKey,
|
||||
key,
|
||||
|
@ -613,6 +648,16 @@ export function getGoogleCalendarCredential() {
|
|||
});
|
||||
}
|
||||
|
||||
export function getAppleCalendarCredential() {
|
||||
return getMockedCredential({
|
||||
metadataLookupKey: "applecalendar",
|
||||
key: {
|
||||
scope:
|
||||
"https://www.applecalendar.example/auth/calendar.events https://www.applecalendar.example/auth/calendar.readonly",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function getZoomAppCredential() {
|
||||
return getMockedCredential({
|
||||
metadataLookupKey: "zoomvideo",
|
||||
|
@ -788,6 +833,7 @@ export function getOrganizer({
|
|||
selectedCalendars,
|
||||
destinationCalendar,
|
||||
defaultScheduleId,
|
||||
weekStart = "Sunday",
|
||||
teams,
|
||||
}: {
|
||||
name: string;
|
||||
|
@ -798,6 +844,7 @@ export function getOrganizer({
|
|||
selectedCalendars?: InputSelectedCalendar[];
|
||||
defaultScheduleId?: number | null;
|
||||
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
|
||||
weekStart?: WeekDays;
|
||||
teams?: InputUser["teams"];
|
||||
}) {
|
||||
return {
|
||||
|
@ -811,6 +858,7 @@ export function getOrganizer({
|
|||
selectedCalendars,
|
||||
destinationCalendar,
|
||||
defaultScheduleId,
|
||||
weekStart,
|
||||
teams,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -115,8 +115,9 @@
|
|||
"@types/node": "16.9.1",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"libphonenumber-js@^1.10.12": "patch:libphonenumber-js@npm%3A1.10.12#./.yarn/patches/libphonenumber-js-npm-1.10.12-51c84f8bf1.patch",
|
||||
"next-i18next@^13.2.2": "patch:next-i18next@npm%3A13.3.0#./.yarn/patches/next-i18next-npm-13.3.0-bf25b0943c.patch"
|
||||
"next-i18next@^13.2.2": "patch:next-i18next@npm%3A13.3.0#./.yarn/patches/next-i18next-npm-13.3.0-bf25b0943c.patch",
|
||||
"libphonenumber-js@^1.10.51": "patch:libphonenumber-js@npm%3A1.10.51#./.yarn/patches/libphonenumber-js-npm-1.10.51-4ff79b15f8.patch",
|
||||
"libphonenumber-js@^1.10.12": "patch:libphonenumber-js@npm%3A1.10.51#./.yarn/patches/libphonenumber-js-npm-1.10.51-4ff79b15f8.patch"
|
||||
},
|
||||
"lint-staged": {
|
||||
"(apps|packages)/**/*.{js,ts,jsx,tsx}": [
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Toaster } from "react-hot-toast";
|
|||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, Button, Form, TextField } from "@calcom/ui";
|
||||
import { Alert, Button, Form, PasswordField, TextField } from "@calcom/ui";
|
||||
|
||||
export default function AppleCalendarSetup() {
|
||||
const { t } = useLocale();
|
||||
|
@ -20,8 +20,8 @@ export default function AppleCalendarSetup() {
|
|||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
return (
|
||||
<div className="bg-emphasis flex h-screen">
|
||||
<div className="bg-default m-auto rounded p-5 md:w-[560px] md:p-10">
|
||||
<div className="bg-emphasis flex h-screen dark:bg-inherit">
|
||||
<div className="bg-default dark:bg-muted border-subtle m-auto rounded p-5 dark:border md:w-[560px] md:p-10">
|
||||
<div className="flex flex-col space-y-5 md:flex-row md:space-x-5 md:space-y-0">
|
||||
<div>
|
||||
{/* eslint-disable @next/next/no-img-element */}
|
||||
|
@ -32,12 +32,14 @@ export default function AppleCalendarSetup() {
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-default">{t("connect_apple_server")}</h1>
|
||||
<h1 className="text-default dark:text-emphasis mb-3 font-semibold">
|
||||
{t("connect_apple_server")}
|
||||
</h1>
|
||||
|
||||
<div className="mt-1 text-sm">
|
||||
{t("apple_server_generate_password", { appName: APP_NAME })}{" "}
|
||||
<a
|
||||
className="text-indigo-400"
|
||||
className="font-bold hover:underline"
|
||||
href="https://appleid.apple.com/account/manage"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
|
@ -45,7 +47,7 @@ export default function AppleCalendarSetup() {
|
|||
</a>
|
||||
. {t("credentials_stored_encrypted")}
|
||||
</div>
|
||||
<div className="my-2 mt-3">
|
||||
<div className="my-2 mt-4">
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={async (values) => {
|
||||
|
@ -65,7 +67,7 @@ export default function AppleCalendarSetup() {
|
|||
}
|
||||
}}>
|
||||
<fieldset
|
||||
className="space-y-2"
|
||||
className="space-y-4"
|
||||
disabled={form.formState.isSubmitting}
|
||||
data-testid="apple-calendar-form">
|
||||
<TextField
|
||||
|
@ -76,9 +78,8 @@ export default function AppleCalendarSetup() {
|
|||
placeholder="appleid@domain.com"
|
||||
data-testid="apple-calendar-email"
|
||||
/>
|
||||
<TextField
|
||||
<PasswordField
|
||||
required
|
||||
type="password"
|
||||
{...form.register("password")}
|
||||
label={t("password")}
|
||||
placeholder="•••••••••••••"
|
||||
|
|
|
@ -574,26 +574,27 @@ export default class EventManager {
|
|||
(c) => c.type === destination.integration
|
||||
);
|
||||
// It might not be the first connected calendar as it seems that the order is not guaranteed to be ascending of credentialId.
|
||||
const firstCalendarCredential = destinationCalendarCredentials[0];
|
||||
const firstCalendarCredential = destinationCalendarCredentials[0] as
|
||||
| (typeof destinationCalendarCredentials)[number]
|
||||
| undefined;
|
||||
|
||||
if (!firstCalendarCredential) {
|
||||
log.warn(
|
||||
"No other credentials found of the same type as the destination calendar. Falling back to first connected calendar"
|
||||
);
|
||||
await fallbackToFirstConnectedCalendar();
|
||||
}
|
||||
|
||||
} else {
|
||||
log.warn(
|
||||
"No credentialId found for destination calendar, falling back to first found calendar",
|
||||
"No credentialId found for destination calendar, falling back to first found calendar of same type as destination calendar",
|
||||
safeStringify({
|
||||
destination: getPiiFreeDestinationCalendar(destination),
|
||||
firstConnectedCalendar: getPiiFreeCredential(firstCalendarCredential),
|
||||
})
|
||||
);
|
||||
|
||||
createdEvents.push(await createEvent(firstCalendarCredential, event));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn(
|
||||
"No destination Calendar found, falling back to first connected calendar",
|
||||
|
|
|
@ -7,7 +7,7 @@ import logger from "@calcom/lib/logger";
|
|||
import { getPiiFreeBooking } from "@calcom/lib/piiFreeData";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { SelectedCalendar } from "@calcom/prisma/client";
|
||||
import type { Prisma, SelectedCalendar } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { EventBusyDetails } from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
@ -83,9 +83,10 @@ export async function getBusyTimes(params: {
|
|||
const endTimeDate =
|
||||
rescheduleUid && duration ? dayjs(endTime).add(duration, "minute").toDate() : new Date(endTime);
|
||||
|
||||
// startTime is less than endTimeDate and endTime grater than startTimeDate
|
||||
const sharedQuery = {
|
||||
startTime: { gte: startTimeDate },
|
||||
endTime: { lte: endTimeDate },
|
||||
startTime: { lte: endTimeDate },
|
||||
endTime: { gte: startTimeDate },
|
||||
status: {
|
||||
in: [BookingStatus.ACCEPTED],
|
||||
},
|
||||
|
@ -264,8 +265,9 @@ export async function getBusyTimesForLimitChecks(params: {
|
|||
eventTypeId: number;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
rescheduleUid?: string | null;
|
||||
}) {
|
||||
const { userId, eventTypeId, startDate, endDate } = params;
|
||||
const { userId, eventTypeId, startDate, endDate, rescheduleUid } = params;
|
||||
logger.silly(
|
||||
`Fetch limit checks bookings in range ${startDate} to ${endDate} for input ${JSON.stringify({
|
||||
userId,
|
||||
|
@ -275,8 +277,7 @@ export async function getBusyTimesForLimitChecks(params: {
|
|||
);
|
||||
performance.mark("getBusyTimesForLimitChecksStart");
|
||||
|
||||
const bookings = await prisma.booking.findMany({
|
||||
where: {
|
||||
const where: Prisma.BookingWhereInput = {
|
||||
userId,
|
||||
eventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
|
@ -287,7 +288,16 @@ export async function getBusyTimesForLimitChecks(params: {
|
|||
endTime: {
|
||||
lte: endDate,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (rescheduleUid) {
|
||||
where.NOT = {
|
||||
uid: rescheduleUid,
|
||||
};
|
||||
}
|
||||
|
||||
const bookings = await prisma.booking.findMany({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
startTime: true,
|
||||
|
|
|
@ -215,7 +215,8 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
|||
dateTo,
|
||||
duration,
|
||||
eventType,
|
||||
user.id
|
||||
user.id,
|
||||
initialData?.rescheduleUid
|
||||
)
|
||||
: [];
|
||||
|
||||
|
@ -419,7 +420,8 @@ const _getBusyTimesFromLimits = async (
|
|||
dateTo: Dayjs,
|
||||
duration: number | undefined,
|
||||
eventType: NonNullable<EventType>,
|
||||
userId: number
|
||||
userId: number,
|
||||
rescheduleUid?: string | null
|
||||
) => {
|
||||
performance.mark("limitsStart");
|
||||
|
||||
|
@ -445,6 +447,7 @@ const _getBusyTimesFromLimits = async (
|
|||
eventTypeId: eventType.id,
|
||||
startDate: limitDateFrom.toDate(),
|
||||
endDate: limitDateTo.toDate(),
|
||||
rescheduleUid: rescheduleUid,
|
||||
});
|
||||
|
||||
// run this first, as counting bookings should always run faster..
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
```ts
|
||||
import { renderEmail } from "@calcom/emails";
|
||||
|
||||
renderEmail("TeamInviteEmail", {
|
||||
await renderEmail("TeamInviteEmail", {
|
||||
language: t,
|
||||
from: "teampro@example.com",
|
||||
to: "pro@example.com",
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"@calcom/embed-snippet": ["../embed-snippet/src"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "env.d.ts"],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "env.d.ts"],
|
||||
// Exclude "test" because that has `api.test.ts` which imports @calcom/embed-react which needs it to be built using this tsconfig.json first. Excluding it here prevents type-check from validating test folder
|
||||
"exclude": ["node_modules", "test"]
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user