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

This commit is contained in:
Leo Giovanetti 2023-06-06 19:57:02 -03:00
commit 8032a86581
32 changed files with 285 additions and 127 deletions

View File

@ -6,13 +6,13 @@
import type { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
import { z } from "zod";
import { getCachedResults } from "@calcom/core";
import getCalendarsEvents from "@calcom/core/getCalendarsEvents";
import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
const paramsSchema = z.object({ user: z.string(), month: z.string() });
export const getStaticProps: GetStaticProps<
{ results: Awaited<ReturnType<typeof getCachedResults>> },
{ results: Awaited<ReturnType<typeof getCalendarsEvents>> },
{ user: string }
> = async (context) => {
const { user: username, month } = paramsSchema.parse(context.params);
@ -33,7 +33,7 @@ export const getStaticProps: GetStaticProps<
const endDate = dayjs.utc(month, "YYYY-MM").endOf("month").add(14, "hours").format();
try {
const results = userWithCredentials?.credentials
? await getCachedResults(
? await getCalendarsEvents(
userWithCredentials?.credentials,
startDate,
endDate,

View File

@ -48,6 +48,10 @@ class MyDocument extends Document<Props> {
<meta name="msapplication-TileColor" content="#ff0000" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f9fafb" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
/>
</Head>
<body

View File

@ -26,7 +26,7 @@ const decryptedSchema = z.object({
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
const { action, token, reason } = querySchema.parse(req.query);
const { bookingUid } = decryptedSchema.parse(
const { bookingUid, userId } = decryptedSchema.parse(
JSON.parse(symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || ""))
);
@ -34,10 +34,19 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
where: { uid: bookingUid },
});
const user = await prisma.user.findUniqueOrThrow({
where: { id: userId },
});
try {
/** @see https://trpc.io/docs/server-side-calls */
const ctx = await createContext({ req, res });
const caller = viewerRouter.createCaller({ ...ctx, req, res });
const caller = viewerRouter.createCaller({
...ctx,
req,
res,
user: { ...user, locale: user?.locale ?? "en" },
});
await caller.bookings.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,

View File

@ -9,7 +9,7 @@ test.afterEach(({ users }) => users.deleteAll());
test.describe("App Store - Authed", () => {
test("Browse apple-calendar and try to install", async ({ page, users }) => {
const pro = await users.create();
await pro.login();
await pro.apiLogin();
await page.goto("/apps/categories/calendar");
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
await page.waitForURL("/apps/apple-calendar");
@ -19,7 +19,7 @@ test.describe("App Store - Authed", () => {
test("Installed Apps - Navigation", async ({ page, users }) => {
const user = await users.create();
await user.login();
await user.apiLogin();
await page.goto("/apps/installed");
await page.waitForSelector('[data-testid="connect-calendar-apps"]');
await page.click('[data-testid="vertical-tab-payment"]');

View File

@ -3,7 +3,7 @@ import { test } from "../lib/fixtures";
test.describe("Can signup from a team invite", async () => {
test.beforeEach(async ({ users }) => {
const proUser = await users.create();
await proUser.login();
await proUser.apiLogin();
});
test.afterEach(async ({ users }) => users.deleteAll());
@ -33,65 +33,65 @@ test.describe("Can signup from a team invite", async () => {
// Wait for the invite to be sent
/*await page.waitForSelector(`[data-testid="member-email"][data-email="${testUser.email}"]`);
const tokenObj = await prisma.verificationToken.findFirstOrThrow({
where: { identifier: testUser.email },
select: { token: true },
});
const tokenObj = await prisma.verificationToken.findFirstOrThrow({
where: { identifier: testUser.email },
select: { token: true },
});
if (!proUser.username) throw Error("Test username is null, can't continue");
if (!proUser.username) throw Error("Test username is null, can't continue");
// Open a new user window to accept the invite
const newPage = await browser.newPage();
await newPage.goto(`/auth/signup?token=${tokenObj.token}&callbackUrl=${WEBAPP_URL}/settings/teams`);
// Open a new user window to accept the invite
const newPage = await browser.newPage();
await newPage.goto(`/auth/signup?token=${tokenObj.token}&callbackUrl=${WEBAPP_URL}/settings/teams`);
// Fill in form
await newPage.fill('input[name="username"]', proUser.username); // Invalid username
await newPage.fill('input[name="email"]', testUser.email);
await newPage.fill('input[name="password"]', testUser.password);
await newPage.fill('input[name="passwordcheck"]', testUser.password);
await newPage.press('input[name="passwordcheck"]', "Enter"); // Press Enter to submit
// Fill in form
await newPage.fill('input[name="username"]', proUser.username); // Invalid username
await newPage.fill('input[name="email"]', testUser.email);
await newPage.fill('input[name="password"]', testUser.password);
await newPage.fill('input[name="passwordcheck"]', testUser.password);
await newPage.press('input[name="passwordcheck"]', "Enter"); // Press Enter to submit
await expect(newPage.locator('text="Username already taken"')).toBeVisible();
await expect(newPage.locator('text="Username already taken"')).toBeVisible();
// Email address is already registered
// TODO: Form errors don't disappear when corrected and resubmitted, so we need to refresh
await newPage.reload();
await newPage.fill('input[name="username"]', testUser.username);
await newPage.fill('input[name="email"]', `${proUser.username}@example.com`); // Taken email
await newPage.fill('input[name="password"]', testUser.password);
await newPage.fill('input[name="passwordcheck"]', testUser.password);
await newPage.press('input[name="passwordcheck"]', "Enter"); // Press Enter to submit
await expect(newPage.locator('text="Email address is already registered"')).toBeVisible();
// Email address is already registered
// TODO: Form errors don't disappear when corrected and resubmitted, so we need to refresh
await newPage.reload();
await newPage.fill('input[name="username"]', testUser.username);
await newPage.fill('input[name="email"]', `${proUser.username}@example.com`); // Taken email
await newPage.fill('input[name="password"]', testUser.password);
await newPage.fill('input[name="passwordcheck"]', testUser.password);
await newPage.press('input[name="passwordcheck"]', "Enter"); // Press Enter to submit
await expect(newPage.locator('text="Email address is already registered"')).toBeVisible();
// Successful signup
// TODO: Form errors don't disappear when corrected and resubmitted, so we need to refresh
await newPage.reload();
await newPage.fill('input[name="username"]', testUser.username);
await newPage.fill('input[name="email"]', testUser.email);
await newPage.fill('input[name="password"]', testUser.password);
await newPage.fill('input[name="passwordcheck"]', testUser.password);
await newPage.press('input[name="passwordcheck"]', "Enter"); // Press Enter to submit
await expect(newPage.locator(`[data-testid="login-form"]`)).toBeVisible();
// Successful signup
// TODO: Form errors don't disappear when corrected and resubmitted, so we need to refresh
await newPage.reload();
await newPage.fill('input[name="username"]', testUser.username);
await newPage.fill('input[name="email"]', testUser.email);
await newPage.fill('input[name="password"]', testUser.password);
await newPage.fill('input[name="passwordcheck"]', testUser.password);
await newPage.press('input[name="passwordcheck"]', "Enter"); // Press Enter to submit
await expect(newPage.locator(`[data-testid="login-form"]`)).toBeVisible();
// We don't need the new browser anymore
await newPage.close();
// We don't need the new browser anymore
await newPage.close();
const createdUser = await prisma.user.findUniqueOrThrow({
where: { email: testUser.email },
include: { teams: { include: { team: true } } },
});
const createdUser = await prisma.user.findUniqueOrThrow({
where: { email: testUser.email },
include: { teams: { include: { team: true } } },
});
console.log("createdUser", createdUser);
console.log("createdUser", createdUser);
// Check that the user was created
expect(createdUser).not.toBeNull();
expect(createdUser.username).toBe(testUser.username);
expect(createdUser.password).not.toBeNull();
expect(createdUser.emailVerified).not.toBeNull();
// Check that the user accepted the team invite
expect(createdUser.teams).toHaveLength(1);
expect(createdUser.teams[0].team.name).toBe(teamName);
expect(createdUser.teams[0].role).toBe("MEMBER");
expect(createdUser.teams[0].accepted).toBe(true);*/
// Check that the user was created
expect(createdUser).not.toBeNull();
expect(createdUser.username).toBe(testUser.username);
expect(createdUser.password).not.toBeNull();
expect(createdUser.emailVerified).not.toBeNull();
// Check that the user accepted the team invite
expect(createdUser.teams).toHaveLength(1);
expect(createdUser.teams[0].team.name).toBe(teamName);
expect(createdUser.teams[0].role).toBe("MEMBER");
expect(createdUser.teams[0].accepted).toBe(true);*/
});
});

View File

@ -8,12 +8,10 @@ test("Can delete user account", async ({ page, users }) => {
const user = await users.create({
username: "delete-me",
});
await user.login();
await user.apiLogin();
await page.goto(`/settings/my-account/profile`);
await page.waitForSelector("[data-testid=dashboard-shell]");
await page.goto(`/settings/my-account/profile`);
await page.waitForLoadState("networkidle");
await page.click("[data-testid=delete-account]");
expect(user.username).toBeTruthy();

View File

@ -18,7 +18,7 @@ test("Can reset forgotten password", async ({ page, users }) => {
// Press Enter
await Promise.all([
page.waitForURL((u) => u.pathname.startsWith("/auth/forgot-password/")),
page.waitForURL((u) => u.pathname.startsWith("/auth/forgot-password")),
page.press('input[name="email"]', "Enter"),
]);

View File

@ -9,7 +9,7 @@ test.describe.configure({ mode: "parallel" });
test.describe("Availablity tests", () => {
test.beforeEach(async ({ page, users }) => {
const user = await users.create();
await user.login();
await user.apiLogin();
await page.goto("/availability");
// We wait until loading is finished
await page.waitForSelector('[data-testid="schedules"]');

View File

@ -78,7 +78,7 @@ testBothBookers.describe("pro user", () => {
const [eventType] = pro.eventTypes;
await bookings.create(pro.id, pro.username, eventType.id);
await pro.login();
await pro.apiLogin();
await page.goto("/bookings/upcoming");
await page.waitForSelector('[data-testid="bookings"]');
await page.locator('[data-testid="edit_booking"]').nth(0).click();
@ -106,7 +106,7 @@ testBothBookers.describe("pro user", () => {
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
const [pro] = users.get();
await pro.login();
await pro.apiLogin();
await page.goto("/bookings/upcoming");
await page.locator('[data-testid="cancel"]').first().click();
@ -132,7 +132,7 @@ testBothBookers.describe("pro user", () => {
}) => {
await bookOptinEvent(page);
const [pro] = users.get();
await pro.login();
await pro.apiLogin();
await page.goto("/bookings/unconfirmed");
await Promise.all([

View File

@ -14,6 +14,7 @@ import {
selectFirstAvailableTimeSlotNextMonth,
} from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
test.afterEach(({ users }) => users.deleteAll());
async function createUserWithSeatedEvent(users: Fixtures["users"]) {
@ -50,17 +51,27 @@ async function createUserWithSeatedEventAndAttendees(
testBothBookers.describe("Booking with Seats", (bookerVariant) => {
test("User can create a seated event (2 seats as example)", async ({ users, page }) => {
const user = await users.create({ name: "Seated event" });
await user.login();
await user.apiLogin();
await page.goto("/event-types");
// We wait until loading is finished
await page.waitForSelector('[data-testid="event-types"]');
const eventTitle = "My 2-seated event";
await createNewSeatedEventType(page, { eventTitle });
await expect(page.locator(`text=${eventTitle} event type updated successfully`)).toBeVisible();
});
test("Multiple Attendees can book a seated event time slot", async ({ users, page }) => {
const slug = "my-2-seated-event";
const user = await users.create({
name: "Seated event user",
eventTypes: [
{ title: "My 2-seated event", slug, length: 60, seatsPerTimeSlot: 2, seatsShowAttendees: true },
{
title: "My 2-seated event",
slug,
length: 60,
seatsPerTimeSlot: 2,
seatsShowAttendees: true,
},
],
});
await page.goto(`/${user.username}/${slug}`);
@ -93,6 +104,7 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
await expect(page.locator("[data-testid=success-page]")).toBeHidden();
});
});
// TODO: Make E2E test: Attendee #1 should be able to cancel his booking
// todo("Attendee #1 should be able to cancel his booking");
// TODO: Make E2E test: Attendee #1 should be able to reschedule his booking
@ -222,7 +234,7 @@ testBothBookers.describe("Reschedule for booking with seats", () => {
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
]);
await user.login();
await user.apiLogin();
const oldBooking = await prisma.booking.findFirst({
where: { uid: booking.uid },
@ -328,7 +340,7 @@ testBothBookers.describe("Reschedule for booking with seats", () => {
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
]);
await user.login();
await user.apiLogin();
const bookingAttendees = await prisma.attendee.findMany({
where: { bookingId: booking.id },
@ -379,7 +391,7 @@ testBothBookers.describe("Reschedule for booking with seats", () => {
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
{ name: "Jane Second", email: "second+seats@cal.com", timeZone: "Europe/Berlin" },
]);
await user.login();
await user.apiLogin();
const bookingWithEventType = await prisma.booking.findFirst({
where: { uid: booking.uid },
select: {

View File

@ -7,7 +7,7 @@ test.afterEach(({ users }) => users.deleteAll());
test.describe("Change Password Test", () => {
test("change password", async ({ page, users }) => {
const pro = await users.create();
await pro.login();
await pro.apiLogin();
// Go to http://localhost:3000/settings/security
await page.goto("/settings/security/password");

View File

@ -26,7 +26,7 @@ test.describe("Change username on settings", () => {
test.fixme("User can change username", async ({ page, users, prisma }) => {
const user = await users.create();
await user.login();
await user.apiLogin();
// Try to go homepage
await page.goto("/settings/my-account/profile");
// Change username from normal to normal
@ -56,7 +56,7 @@ test.describe("Change username on settings", () => {
const user = await users.create();
await stripe.customers.create({ email: `${user?.username}@example.com` });
await user.login();
await user.apiLogin();
await page.goto("/settings/my-account/profile");
// Change username from normal to premium

View File

@ -14,7 +14,7 @@ test.afterEach(({ users }) => users.deleteAll());
// eslint-disable-next-line playwright/no-skipped-test
test.skip("dynamic booking", async ({ page, users }) => {
const pro = await users.create();
await pro.login();
await pro.apiLogin();
const free = await users.create({ username: "free" });
await page.goto(`/${pro.username}+${free.username}`);

View File

@ -47,7 +47,15 @@ async function expectToBeNavigatingToEmbedTypesDialog(
async function expectToBeNavigatingToEmbedCodeAndPreviewDialog(
page: Page,
{ embedUrl, embedType, basePage }: { embedUrl: string | null; embedType: string; basePage: string }
{
embedUrl,
embedType,
basePage,
}: {
embedUrl: string | null;
embedType: string;
basePage: string;
}
) {
if (!embedUrl) {
throw new Error("Couldn't find embedUrl");
@ -89,7 +97,7 @@ test.afterEach(({ users }) => users.deleteAll());
test.describe("Embed Code Generator Tests", () => {
test.beforeEach(async ({ users }) => {
const pro = await users.create();
await pro.login();
await pro.apiLogin();
});
test.describe("Event Types Page", () => {

View File

@ -13,7 +13,7 @@ test.describe("Event Types tests", () => {
testBothBookers.describe("user", (bookerVariant) => {
test.beforeEach(async ({ page, users }) => {
const user = await users.create();
await user.login();
await user.apiLogin();
await page.goto("/event-types");
// We wait until loading is finished
await page.waitForSelector('[data-testid="event-types"]');

View File

@ -262,13 +262,17 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
// self is a reflective method that return the Prisma object that references this fixture.
const self = async () =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(await prisma.user.findUnique({ where: { id: store.user.id }, include: { eventTypes: true } }))!;
(await prisma.user.findUnique({
where: { id: store.user.id },
include: { eventTypes: true },
}))!;
return {
id: user.id,
username: user.username,
eventTypes: user.eventTypes,
routingForms: user.routingForms,
self,
apiLogin: async () => apiLogin({ ...(await self()), password: user.username }, store.page),
login: async () => login({ ...(await self()), password: user.username }, store.page),
logout: async () => {
await page.goto("/auth/logout");
@ -276,7 +280,10 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
getPaymentCredential: async () => getPaymentCredential(store.page),
// ths is for developemnt only aimed to inject debugging messages in the metadata field of the user
debug: async (message: string | Record<string, JSONValue>) => {
await prisma.user.update({ where: { id: store.user.id }, data: { metadata: { debug: message } } });
await prisma.user.update({
where: { id: store.user.id },
data: { metadata: { debug: message } },
});
},
delete: async () => await prisma.user.delete({ where: { id: store.user.id } }),
};
@ -342,6 +349,28 @@ export async function login(
await page.waitForLoadState("networkidle");
}
export async function apiLogin(
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "password" | "email">>,
page: Page
) {
const csrfToken = await page
.context()
.request.get("/api/auth/csrf")
.then((response) => response.json())
.then((json) => json.csrfToken);
const data = {
email: user.email ?? `${user.username}@example.com`,
password: user.password ?? user.username!,
callbackURL: "http://localhost:3000/",
redirect: "false",
json: "true",
csrfToken,
};
return page.context().request.post("/api/auth/callback/credentials", {
data,
});
}
export async function getPaymentCredential(page: Page) {
await page.goto("/apps/stripe");

View File

@ -9,7 +9,7 @@ test.describe.configure({ mode: "parallel" });
test.fixme("hash my url", () => {
test.beforeEach(async ({ users }) => {
const user = await users.create();
await user.login();
await user.apiLogin();
});
test.afterEach(async ({ users }) => {
await users.deleteAll();

View File

@ -24,7 +24,7 @@ test.describe("Stripe integration", () => {
test.describe("Stripe integration dashboard", () => {
test("Can add Stripe integration", async ({ page, users }) => {
const user = await users.create();
await user.login();
await user.apiLogin();
await page.goto("/apps/installed");
await user.getPaymentCredential();
@ -39,7 +39,7 @@ test.describe("Stripe integration", () => {
test("Can book a paid booking", async ({ page, users }) => {
const user = await users.create();
const eventType = user.eventTypes.find((e) => e.slug === "paid");
await user.login();
await user.apiLogin();
await page.goto("/apps/installed");
await user.getPaymentCredential();

View File

@ -214,7 +214,7 @@ test.fixme("Integrations", () => {
test.describe("Zoom App", () => {
test("Can add integration", async ({ page, users }) => {
const user = await users.create();
await user.login();
await user.apiLogin();
await addZoomIntegration({ page });
await page.waitForNavigation({
url: (url) => {
@ -226,7 +226,7 @@ test.fixme("Integrations", () => {
test("can choose zoom as a location during booking", async ({ page, users }) => {
const user = await users.create();
await user.login();
await user.apiLogin();
const eventType = await addLocationIntegrationToFirstEvent({ user });
await addZoomIntegration({ page });
await page.waitForNavigation({
@ -241,7 +241,7 @@ test.fixme("Integrations", () => {
// POST https://api.zoom.us/v2/users/me/meetings
// Verify Header-> Authorization: "Bearer " + accessToken,
/**
* {
* {
topic: event.title,
type: 2, // Means that this is a scheduled meeting
start_time: event.startTime,
@ -262,15 +262,15 @@ test.fixme("Integrations", () => {
approval_type: 2,
audio: "both",
auto_recording: "none",
enforce_login: false,
enforce_apiLogin: false,
registrants_email_notification: true,
},
};
*/
*/
});
test("Can disconnect from integration", async ({ page, users }) => {
const user = await users.create();
await user.login();
await user.apiLogin();
await addZoomIntegration({ page });
await page.waitForNavigation({
url: (url) => {
@ -299,7 +299,7 @@ test.fixme("Integrations", () => {
test.describe("Hubspot App", () => {
test("Can add integration", async ({ page, users }) => {
const user = await users.create();
await user.login();
await user.apiLogin();
await addOauthBasedIntegration({
page,
slug: "hubspot",

View File

@ -40,7 +40,7 @@ declare global {
* @see https://playwright.dev/docs/test-fixtures
*/
export const test = base.extend<Fixtures>({
users: async ({ page }, use, workerInfo) => {
users: async ({ page, context }, use, workerInfo) => {
const usersFixture = createUsersFixture(page, workerInfo);
await use(usersFixture);
},

View File

@ -0,0 +1,21 @@
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
test.afterEach(({ users }) => users.deleteAll());
test.describe("Login with api request", () => {
test("context request will share cookie storage with its browser context", async ({ page, users }) => {
const pro = await users.create();
await pro.apiLogin();
const contextCookies = await page.context().cookies();
const cookiesMap = new Map(contextCookies.map(({ name, value }) => [name, value]));
// The browser context will already contain all the cookies from the API response.
expect(cookiesMap.has("next-auth.csrf-token")).toBeTruthy();
expect(cookiesMap.has("next-auth.callback-url")).toBeTruthy();
expect(cookiesMap.has("next-auth.session-token")).toBeTruthy();
});
});

View File

@ -15,6 +15,7 @@ async function getLabelText(field: Locator) {
return await field.locator("label").first().locator("span").first().innerText();
}
test.describe.configure({ mode: "parallel" });
test.describe("Manage Booking Questions", () => {
test.afterEach(async ({ users }) => {
await users.deleteAll();
@ -28,7 +29,7 @@ test.describe("Manage Booking Questions", () => {
}, testInfo) => {
// Considering there are many steps in it, it would need more than default test timeout
test.setTimeout(testInfo.timeout * 3);
const user = await createAndLoginUserWithEventTypes({ users });
const user = await createAndLoginUserWithEventTypes({ users, page });
const webhookReceiver = await addWebhook(user);
@ -51,7 +52,7 @@ test.describe("Manage Booking Questions", () => {
}, testInfo) => {
// Considering there are many steps in it, it would need more than default test timeout
test.setTimeout(testInfo.timeout * 3);
const user = await createAndLoginUserWithEventTypes({ users });
const user = await createAndLoginUserWithEventTypes({ users, page });
const team = await prisma.team.findFirst({
where: {
members: {
@ -247,6 +248,7 @@ async function bookTimeSlot({ page, name, email }: { page: Page; name: string; e
await page.fill('[name="email"]', email);
await page.press('[name="email"]', "Enter");
}
/**
* 'option' starts from 1
*/
@ -358,11 +360,20 @@ async function toggleQuestionRequireStatusAndSave({
await saveEventType(page);
}
async function createAndLoginUserWithEventTypes({ users }: { users: ReturnType<typeof createUsersFixture> }) {
async function createAndLoginUserWithEventTypes({
users,
page,
}: {
users: ReturnType<typeof createUsersFixture>;
page: Page;
}) {
const user = await users.create(null, {
hasTeam: true,
});
await user.login();
await user.apiLogin();
await page.goto("/event-types");
// We wait until loading is finished
await page.waitForSelector('[data-testid="event-types"]');
return user;
}

View File

@ -11,7 +11,8 @@ test.describe("Managed Event Types tests", () => {
// Creating the member user of the team
const memberUser = await users.create();
// First we work with owner user, logging in
await adminUser.login();
await adminUser.apiLogin();
await page.goto("/event-types");
// Making sure page loads completely
await page.waitForLoadState("networkidle");
// Let's create a team
@ -58,7 +59,9 @@ test.describe("Managed Event Types tests", () => {
// Now we need to accept the invitation as member and come back in as admin to
// assign the member in the managed event type
await adminUser.logout();
await memberUser.login();
await memberUser.apiLogin();
await page.goto("/event-types");
// We wait until loading is finished
await page.waitForSelector('[data-testid="event-types"]');
await page.goto("/teams");
await page.waitForLoadState("networkidle");
@ -67,7 +70,8 @@ test.describe("Managed Event Types tests", () => {
await memberUser.logout();
// Coming back as team owner to assign member user to managed event
await adminUser.login();
await adminUser.apiLogin();
await page.goto("/event-types");
await page.waitForLoadState("networkidle");
await page.locator('[data-testid="event-types"] a[title="managed"]').click();
await page.locator('[data-testid="vertical-tab-assignment"]').click();
@ -78,7 +82,8 @@ test.describe("Managed Event Types tests", () => {
await adminUser.logout();
// Coming back as member user to see if there is a managed event present after assignment
await memberUser.login();
await memberUser.apiLogin();
await page.goto("/event-types");
await page.waitForLoadState("networkidle");
await expect(page.locator('[data-testid="event-types"] a[title="managed"]')).toBeVisible();
});

View File

@ -11,8 +11,8 @@ test.describe("Onboarding", () => {
test.describe("Onboarding v2", () => {
test("Onboarding Flow", async ({ page, users }) => {
const user = await users.create({ completedOnboarding: false, name: null });
await user.login();
await user.apiLogin();
await page.goto("/getting-started");
// tests whether the user makes it to /getting-started
// after login with completedOnboarding false
await page.waitForURL("/getting-started");

View File

@ -25,7 +25,7 @@ testBothBookers.describe("Reschedule Tests", async () => {
status: BookingStatus.ACCEPTED,
});
await user.login();
await user.apiLogin();
await page.goto("/bookings/upcoming");
await page.locator('[data-testid="edit_booking"]').nth(0).click();
@ -69,7 +69,7 @@ testBothBookers.describe("Reschedule Tests", async () => {
rescheduled: true,
});
await user.login();
await user.apiLogin();
await page.goto("/bookings/cancelled");
const requestRescheduleSentElement = page.locator('[data-testid="request_reschedule_sent"]').nth(1);
@ -117,7 +117,7 @@ testBothBookers.describe("Reschedule Tests", async () => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(!IS_STRIPE_ENABLED, "Skipped as Stripe is not installed");
const user = await users.create();
await user.login();
await user.apiLogin();
await user.getPaymentCredential();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const eventType = user.eventTypes.find((e) => e.slug === "paid")!;
@ -158,7 +158,7 @@ testBothBookers.describe("Reschedule Tests", async () => {
test("Paid rescheduling should go to success page", async ({ page, users, bookings, payments }) => {
const user = await users.create();
await user.login();
await user.apiLogin();
await user.getPaymentCredential();
await users.logout();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@ -207,7 +207,7 @@ testBothBookers.describe("Reschedule Tests", async () => {
const booking = await bookings.create(user.id, user.username, eventType.id, {
status: BookingStatus.ACCEPTED,
});
await user.login();
await user.apiLogin();
await page.goto(`/${user.username}/${eventType.slug}?rescheduleUid=${booking.uid}`);

View File

@ -12,7 +12,7 @@ test.describe("Teams", () => {
test("Can create teams via Wizard", async ({ page, users }) => {
const user = await users.create();
const inviteeEmail = `${user.username}+invitee@example.com`;
await user.login();
await user.apiLogin();
await page.goto("/teams");
await test.step("Can create team", async () => {

View File

@ -18,7 +18,7 @@ test.describe("BOOKING_CREATED", async () => {
const webhookReceiver = createHttpServer();
const user = await users.create();
const [eventType] = user.eventTypes;
await user.login();
await user.apiLogin();
await page.goto(`/settings/developer/webhooks`);
// --- add webhook
@ -154,7 +154,7 @@ test.describe("BOOKING_REJECTED", async () => {
await bookOptinEvent(page);
// --- login as that user
await user.login();
await user.apiLogin();
await page.goto(`/settings/developer/webhooks`);
@ -275,7 +275,7 @@ test.describe("BOOKING_REQUESTED", async () => {
const user = await users.create();
// --- login as that user
await user.login();
await user.apiLogin();
await page.goto(`/settings/developer/webhooks`);

View File

@ -35,7 +35,7 @@ test.describe("Wipe my Cal App Test", () => {
);
await bookings.create(pro.id, pro.username, eventType.id, {});
await bookings.create(pro.id, pro.username, eventType.id, {});
await pro.login();
await pro.apiLogin();
await page.goto("/bookings/upcoming");
await expect(page.locator("data-testid=wipe-today-button")).toBeVisible();

View File

@ -294,6 +294,18 @@
"success": "Succès",
"failed": "Échoué",
"password_has_been_reset_login": "Votre mot de passe a été réinitialisé. Vous pouvez désormais vous connecter avec votre nouveau mot de passe.",
"bookerlayout_title": "Mise en page",
"bookerlayout_default_title": "Vue par défaut",
"bookerlayout_description": "Vous pouvez en sélectionner plusieurs, vos utilisateurs peuvent basculer entre les vues.",
"bookerlayout_user_settings_title": "Mise en page de réservation",
"bookerlayout_user_settings_description": "Vous pouvez en sélectionner plusieurs, les utilisateurs peuvent basculer entre les vues. Cela peut être remplacé individuellement pour événement.",
"bookerlayout_month_view": "Mensuelle",
"bookerlayout_week_view": "Hebdomadaire",
"bookerlayout_column_view": "Colonne",
"bookerlayout_error_min_one_enabled": "Au moins une mise en page doit être activée.",
"bookerlayout_error_default_not_enabled": "La mise en page sélectionnée comme vue par défaut ne fait pas partie des mises en page activées.",
"bookerlayout_error_unknown_layout": "La mise en page que vous avez sélectionnée n'est pas une mise en page valide.",
"bookerlayout_override_global_settings": "Vous pouvez gérer cela pour tous vos types d'événements dans <2>Paramètres / Apparence</2> ou <6>l'appliquer uniquement à cet événement</6>.",
"unexpected_error_try_again": "Une erreur inattendue s'est produite. Veuillez réessayer.",
"sunday_time_error": "Heure non valide le dimanche",
"monday_time_error": "Créneau invalide le lundi",
@ -760,6 +772,9 @@
"new_event_type_to_book_description": "Créez un nouveau type dévénement pour que les personnes puissent effectuer des réservations.",
"length": "Durée",
"minimum_booking_notice": "Préavis minimum",
"offset_toggle": "Décalage de l'heure de début",
"offset_toggle_description": "Appliquez un décalage de durée spécifique aux créneaux horaires affichés aux utilisateurs.",
"offset_start": "Décalage de",
"offset_start_description": "p. ex. cela affichera les créneaux à vos utilisateurs à {{ adjustedTime }} au lieu de {{ originalTime }}",
"slot_interval": "Fréquence de créneaux horaires",
"slot_interval_default": "Utiliser la durée de l'événement (par défaut)",
@ -1268,7 +1283,7 @@
"connect_automation_apps": "Connecter des applications d'automatisation",
"connect_analytics_apps": "Connecter des applications analytiques",
"connect_other_apps": "Connecter d'autres applications",
"connect_web3_apps": "Connecter des apps Web3",
"connect_web3_apps": "Connecter des applications Web3",
"current_step_of_total": "Étape {{currentStep}} sur {{maxSteps}}",
"add_variable": "Ajouter une variable",
"custom_phone_number": "Numéro de téléphone personnalisé",
@ -1288,6 +1303,7 @@
"download_responses_description": "Téléchargez toutes les réponses à votre formulaire au format CSV.",
"download": "Télécharger",
"download_recording": "Télécharger l'enregistrement",
"recording_from_your_recent_call": "Un enregistrement de votre appel récent sur {{appName}} est prêt à être téléchargé",
"create_your_first_form": "Créez votre premier formulaire",
"create_your_first_form_description": "Avec les formulaires de routage, vous pouvez poser des questions de qualification et rediriger vers la bonne personne ou le bon type d'événement.",
"create_your_first_webhook": "Créez votre premier webhook",
@ -1483,6 +1499,8 @@
"find_the_best_person": "Trouvez la meilleure personne disponible et alternez à travers votre équipe.",
"fixed_round_robin": "Round-Robin fixe",
"add_one_fixed_attendee": "Ajoutez un participant fixe et le Round-Robin via un certain nombre de participants.",
"calcom_is_better_with_team": "{{appName}} est meilleur en équipe",
"the_calcom_team": "L'équipe {{companyName}}",
"add_your_team_members": "Ajoutez les membres de votre équipe à vos types d'événements. Utilisez la planification collective pour inclure tout le monde ou trouvez la personne la plus appropriée avec la planification Round-Robin.",
"booking_limit_reached": "La limite de réservation pour ce type d'événement a été atteinte",
"duration_limit_reached": "La limite de durée pour ce type d'événement a été atteinte",
@ -1501,6 +1519,7 @@
"navigate_installed_apps": "Accéder aux applications installées",
"disabled_calendar": "Si un autre calendrier est installé, les nouvelles réservations y seront ajoutées. Si ce n'est pas le cas, connectez un nouveau calendrier pour ne pas manquer de nouvelles réservations.",
"enable_apps": "Activer les applications",
"enable_apps_description": "Activer les applications que les utilisateurs peuvent intégrer à {{appName}}",
"purchase_license": "Acheter une licence",
"already_have_key": "J'ai déjà une clé :",
"already_have_key_suggestion": "Veuillez copier votre variable d'environnement CALCOM_LICENSE_KEY existante ici.",

View File

@ -18,6 +18,8 @@ import type {
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
import type { EventResult } from "@calcom/types/EventManager";
import getCalendarsEvents from "./getCalendarsEvents";
const log = logger.getChildLogger({ prefix: ["CalendarManager"] });
let coldStart = true;
@ -236,7 +238,7 @@ export const getBusyCalendarTimes = async (
const startDate = dayjs(dateFrom).subtract(11, "hours").format();
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
const endDate = dayjs(dateTo).endOf("month").add(14, "hours").format();
results = await getCachedResults(withCredentials, startDate, endDate, selectedCalendars);
results = await getCalendarsEvents(withCredentials, startDate, endDate, selectedCalendars);
logger.info("Generating calendar cache in background");
// on cold start the calendar cache page generated in the background
Promise.all(months.map((month) => createCalendarCachePage(username, month)));

View File

@ -0,0 +1,51 @@
import type { SelectedCalendar } from "@prisma/client";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { performance } from "@calcom/lib/server/perfObserver";
import type { EventBusyDate } from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
const getCalendarsEvents = async (
withCredentials: CredentialPayload[],
dateFrom: string,
dateTo: string,
selectedCalendars: SelectedCalendar[]
): Promise<EventBusyDate[][]> => {
const calendarCredentials = withCredentials.filter((credential) => credential.type.endsWith("_calendar"));
const calendars = await Promise.all(calendarCredentials.map((credential) => getCalendar(credential)));
performance.mark("getBusyCalendarTimesStart");
const results = calendars.map(async (c, i) => {
/** Filter out nulls */
if (!c) return [];
/** We rely on the index so we can match credentials with calendars */
const { type, appId } = calendarCredentials[i];
/** We just pass the calendars that matched the credential type,
* TODO: Migrate credential type or appId
*/
const passedSelectedCalendars = selectedCalendars.filter((sc) => sc.integration === type);
if (!passedSelectedCalendars.length) return [];
/** We extract external Ids so we don't cache too much */
const selectedCalendarIds = passedSelectedCalendars.map((sc) => sc.externalId);
/** If we don't then we actually fetch external calendars (which can be very slow) */
performance.mark("eventBusyDatesStart");
const eventBusyDates = await c.getAvailability(dateFrom, dateTo, passedSelectedCalendars);
performance.mark("eventBusyDatesEnd");
performance.measure(
`[getAvailability for ${selectedCalendarIds.join(", ")}][$1]'`,
"eventBusyDatesStart",
"eventBusyDatesEnd"
);
return eventBusyDates.map((a) => ({ ...a, source: `${appId}` }));
});
const awaitedResults = await Promise.all(results);
performance.mark("getBusyCalendarTimesEnd");
performance.measure(
`getBusyCalendarTimes took $1 for creds ${calendarCredentials.map((cred) => cred.id)}`,
"getBusyCalendarTimesStart",
"getBusyCalendarTimesEnd"
);
return awaitedResults;
};
export default getCalendarsEvents;

View File

@ -5044,7 +5044,7 @@ __metadata:
next: ~13.2.1
next-auth: ^4.20.1
next-i18next: ^11.3.0
playwright: ^1.31.2
playwright-core: ^1.34.3
postcss: ^8.4.18
prism-react-renderer: ^1.3.5
react: ^18.2.0
@ -29630,7 +29630,7 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.34.3":
"playwright-core@npm:^1.34.3":
version: 1.34.3
resolution: "playwright-core@npm:1.34.3"
bin:
@ -29639,17 +29639,6 @@ __metadata:
languageName: node
linkType: hard
"playwright@npm:^1.31.2":
version: 1.34.3
resolution: "playwright@npm:1.34.3"
dependencies:
playwright-core: 1.34.3
bin:
playwright: cli.js
checksum: 4495b23eacc673c03fd4706ce5914dd4855d46657e63411e54bb928e796d7ca59a6101379000ec73e2731437d04a441242cebbb6d4e069e050255db9eff65f7d
languageName: node
linkType: hard
"pngjs@npm:^3.0.0, pngjs@npm:^3.3.3":
version: 3.4.0
resolution: "pngjs@npm:3.4.0"