Skip to content

Commit 148e8b6

Browse files
Merge branch 'main' into image-uploader-storybook
2 parents 9a2d457 + d55b4b0 commit 148e8b6

31 files changed

Lines changed: 619 additions & 219 deletions

File tree

apps/web/components/booking/BookingListItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ function BookingListItem(booking: BookingItemProps) {
336336
</DialogContent>
337337
</Dialog>
338338

339-
<tr className="hover:bg-muted group flex flex-col sm:flex-row">
339+
<tr data-testid="booking-item" className="hover:bg-muted group flex flex-col sm:flex-row">
340340
<td
341341
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
342342
onClick={onClickTableData}>

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@calcom/web",
3-
"version": "3.2.2",
3+
"version": "3.2.3",
44
"private": true,
55
"scripts": {
66
"analyze": "ANALYZE=true next build",

apps/web/pages/api/auth/signup.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
3939
const username = slugify(data.username);
4040
const userEmail = email.toLowerCase();
4141

42-
const validationResponse = (
43-
incomingEmail: string,
44-
validation: { isValid: boolean; email: string | undefined }
45-
) => {
46-
const { isValid, email } = validation;
47-
if (!isValid) {
48-
const message: string =
49-
email !== incomingEmail ? "Username already taken" : "Email address is already registered";
50-
51-
return res.status(409).json({ message });
52-
}
53-
};
54-
5542
if (!username) {
5643
res.status(422).json({ message: "Invalid username" });
5744
return;
@@ -79,11 +66,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
7966
}
8067
if (foundToken?.teamId) {
8168
const teamUserValidation = await validateUsernameInTeam(username, userEmail, foundToken?.teamId);
82-
return validationResponse(userEmail, teamUserValidation);
69+
if (!teamUserValidation.isValid) {
70+
return res.status(409).json({ message: "Username or email is already taken" });
71+
}
8372
}
8473
} else {
8574
const userValidation = await validateUsername(username, userEmail);
86-
return validationResponse(userEmail, userValidation);
75+
if (!userValidation.isValid) {
76+
return res.status(409).json({ message: "Username or email is already taken" });
77+
}
8778
}
8879

8980
const hashedPassword = await hashPassword(password);
@@ -94,7 +85,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
9485
id: foundToken.teamId,
9586
},
9687
});
97-
9888
if (team) {
9989
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
10090

apps/web/pages/bookings/[status].tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export default function Bookings() {
179179
)}
180180
<div className="pt-2 xl:pt-0">
181181
<div className="border-subtle overflow-hidden rounded-md border">
182-
<table className="w-full max-w-full table-fixed">
182+
<table data-testid={`${status}-bookings`} className="w-full max-w-full table-fixed">
183183
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
184184
{query.data.pages.map((page, index) => (
185185
<Fragment key={index}>

apps/web/pages/settings/my-account/profile.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -82,21 +82,20 @@ const ProfileView = () => {
8282
const { data: user, isLoading } = trpc.viewer.me.useQuery();
8383
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
8484
onSuccess: async (res) => {
85+
await update(res);
8586
showToast(t("settings_updated_successfully"), "success");
86-
if (res.signOutUser && tempFormValues) {
87-
if (res.passwordReset) {
88-
showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
89-
// sign out the user to avoid unauthorized access error
90-
await signOut({ callbackUrl: "/auth/logout?passReset=true" });
91-
} else {
92-
// sign out the user to avoid unauthorized access error
93-
await signOut({ callbackUrl: "/auth/logout?emailChange=true" });
94-
}
87+
88+
// signout user only in case of password reset
89+
if (res.signOutUser && tempFormValues && res.passwordReset) {
90+
showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
91+
await signOut({ callbackUrl: "/auth/logout?passReset=true" });
92+
} else {
93+
utils.viewer.me.invalidate();
94+
utils.viewer.avatar.invalidate();
95+
utils.viewer.shouldVerifyEmail.invalidate();
9596
}
96-
utils.viewer.me.invalidate();
97-
utils.viewer.avatar.invalidate();
97+
9898
setConfirmAuthEmailChangeWarningDialogOpen(false);
99-
update(res);
10099
setTempFormValues(null);
101100
},
102101
onError: () => {
@@ -325,7 +324,10 @@ const ProfileView = () => {
325324
{confirmPasswordErrorMessage && <Alert severity="error" title={confirmPasswordErrorMessage} />}
326325
</div>
327326
<DialogFooter showDivider>
328-
<Button color="primary" onClick={(e) => onConfirmPassword(e)}>
327+
<Button
328+
color="primary"
329+
loading={confirmPasswordMutation.isLoading}
330+
onClick={(e) => onConfirmPassword(e)}>
329331
{t("confirm")}
330332
</Button>
331333
<DialogClose />
@@ -345,7 +347,7 @@ const ProfileView = () => {
345347
<DialogFooter>
346348
<Button
347349
color="primary"
348-
disabled={updateProfileMutation.isLoading}
350+
loading={updateProfileMutation.isLoading}
349351
onClick={(e) => onConfirmAuthEmailChange(e)}>
350352
{t("confirm")}
351353
</Button>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { expect } from "@playwright/test";
2+
3+
import { BookingStatus } from "@calcom/prisma/client";
4+
5+
import type { Fixtures } from "./lib/fixtures";
6+
import { test } from "./lib/fixtures";
7+
8+
test.afterEach(({ users }) => users.deleteAll());
9+
10+
test.describe("Bookings", () => {
11+
test.describe("Upcoming bookings", () => {
12+
test("show attendee bookings and organizer bookings in asc order by startDate", async ({
13+
page,
14+
users,
15+
bookings,
16+
}) => {
17+
const firstUser = await users.create();
18+
const secondUser = await users.create();
19+
20+
const bookingWhereFirstUserIsOrganizerFixture = await createBooking({
21+
title: "Booking as organizer",
22+
bookingsFixture: bookings,
23+
// Create a booking 3 days from today
24+
relativeDate: 3,
25+
organizer: firstUser,
26+
organizerEventType: firstUser.eventTypes[0],
27+
attendees: [
28+
{ name: "First", email: "first@cal.com", timeZone: "Europe/Berlin" },
29+
{ name: "Second", email: "second@cal.com", timeZone: "Europe/Berlin" },
30+
{ name: "Third", email: "third@cal.com", timeZone: "Europe/Berlin" },
31+
],
32+
});
33+
const bookingWhereFirstUserIsOrganizer = await bookingWhereFirstUserIsOrganizerFixture.self();
34+
35+
const bookingWhereFirstUserIsAttendeeFixture = await createBooking({
36+
title: "Booking as attendee",
37+
bookingsFixture: bookings,
38+
organizer: secondUser,
39+
// Booking created 2 days from today
40+
relativeDate: 2,
41+
organizerEventType: secondUser.eventTypes[0],
42+
attendees: [
43+
{ name: "OrganizerAsBooker", email: firstUser.email, timeZone: "Europe/Berlin" },
44+
{ name: "Second", email: "second@cal.com", timeZone: "Europe/Berlin" },
45+
{ name: "Third", email: "third@cal.com", timeZone: "Europe/Berlin" },
46+
],
47+
});
48+
const bookingWhereFirstUserIsAttendee = await bookingWhereFirstUserIsAttendeeFixture.self();
49+
50+
await firstUser.apiLogin();
51+
await page.goto(`/bookings/upcoming`);
52+
const upcomingBookings = page.locator('[data-testid="upcoming-bookings"]');
53+
const firstUpcomingBooking = upcomingBookings.locator('[data-testid="booking-item"]').nth(0);
54+
const secondUpcomingBooking = upcomingBookings.locator('[data-testid="booking-item"]').nth(1);
55+
await expect(
56+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
57+
firstUpcomingBooking.locator(`text=${bookingWhereFirstUserIsAttendee!.title}`)
58+
).toBeVisible();
59+
await expect(
60+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
61+
secondUpcomingBooking.locator(`text=${bookingWhereFirstUserIsOrganizer!.title}`)
62+
).toBeVisible();
63+
});
64+
});
65+
});
66+
67+
async function createBooking({
68+
bookingsFixture,
69+
organizer,
70+
organizerEventType,
71+
attendees,
72+
/**
73+
* Relative date from today
74+
* 0 means today
75+
* 1 means tomorrow
76+
*/
77+
relativeDate = 0,
78+
durationMins = 30,
79+
title,
80+
}: {
81+
bookingsFixture: Fixtures["bookings"];
82+
organizer: {
83+
id: number;
84+
username: string | null;
85+
};
86+
organizerEventType: {
87+
id: number;
88+
};
89+
attendees: {
90+
name: string;
91+
email: string;
92+
timeZone: string;
93+
}[];
94+
relativeDate?: number;
95+
durationMins?: number;
96+
title: string;
97+
}) {
98+
const DAY_MS = 24 * 60 * 60 * 1000;
99+
const bookingDurationMs = durationMins * 60 * 1000;
100+
const startTime = new Date(Date.now() + relativeDate * DAY_MS);
101+
const endTime = new Date(Date.now() + relativeDate * DAY_MS + bookingDurationMs);
102+
return await bookingsFixture.create(organizer.id, organizer.username, organizerEventType.id, {
103+
title,
104+
status: BookingStatus.ACCEPTED,
105+
startTime,
106+
endTime,
107+
attendees: {
108+
createMany: {
109+
data: [...attendees],
110+
},
111+
},
112+
});
113+
}

apps/web/playwright/event-types.e2e.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Page } from "@playwright/test";
12
import { expect } from "@playwright/test";
23

34
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -91,6 +92,7 @@ test.describe("Event Types tests", () => {
9192
expect(formTitle).toBe(firstTitle);
9293
expect(formSlug).toContain(firstSlug);
9394
});
95+
9496
test("edit first event", async ({ page }) => {
9597
const $eventTypes = page.locator("[data-testid=event-types] > li a");
9698
const firstEventTypeElement = $eventTypes.first();
@@ -152,5 +154,46 @@ test.describe("Event Types tests", () => {
152154

153155
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
154156
});
157+
158+
test.describe("Different Locations Tests", () => {
159+
test("can add Attendee Phone Number location and book with it", async ({ page }) => {
160+
await gotoFirstEventType(page);
161+
await selectAttendeePhoneNumber(page);
162+
await saveEventType(page);
163+
await gotoBookingPage(page);
164+
await selectFirstAvailableTimeSlotNextMonth(page);
165+
166+
await page.locator(`[data-fob-field-name="location"] input`).fill("9199999999");
167+
await bookTimeSlot(page);
168+
169+
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
170+
await expect(page.locator("text=+19199999999")).toBeVisible();
171+
});
172+
});
155173
});
156174
});
175+
176+
const selectAttendeePhoneNumber = async (page: Page) => {
177+
const locationOptionText = "Attendee Phone Number";
178+
await page.locator("#location-select").click();
179+
await page.locator(`text=${locationOptionText}`).click();
180+
};
181+
182+
async function gotoFirstEventType(page: Page) {
183+
const $eventTypes = page.locator("[data-testid=event-types] > li a");
184+
const firstEventTypeElement = $eventTypes.first();
185+
await firstEventTypeElement.click();
186+
await page.waitForURL((url) => {
187+
return !!url.pathname.match(/\/event-types\/.+/);
188+
});
189+
}
190+
191+
async function saveEventType(page: Page) {
192+
await page.locator("[data-testid=update-eventtype]").click();
193+
}
194+
195+
async function gotoBookingPage(page: Page) {
196+
const previewLink = await page.locator("[data-testid=preview-button]").getAttribute("href");
197+
198+
await page.goto(previewLink ?? "");
199+
}

apps/web/playwright/fixtures/bookings.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ export const createBookingsFixture = (page: Page) => {
1919
username: string | null,
2020
eventTypeId = -1,
2121
{
22+
title = "",
2223
rescheduled = false,
2324
paid = false,
2425
status = "ACCEPTED",
26+
startTime,
27+
endTime,
2528
attendees = {
2629
create: {
2730
email: "attendee@example.com",
@@ -39,9 +42,9 @@ export const createBookingsFixture = (page: Page) => {
3942
const booking = await prisma.booking.create({
4043
data: {
4144
uid: uid,
42-
title: "30min",
43-
startTime: startDate,
44-
endTime: endDateParam || dayjs().add(1, "day").add(30, "minutes").toDate(),
45+
title: title || "30min",
46+
startTime: startTime || startDate,
47+
endTime: endTime || endDateParam || dayjs().add(1, "day").add(30, "minutes").toDate(),
4548
user: {
4649
connect: {
4750
id: userId,

0 commit comments

Comments
 (0)