fix: Dynamic Group Booking link for organization (#12825)
Co-authored-by: Erik <erik@erosemberg.com> Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
parent
31b88c5537
commit
a8975f541f
|
@ -41,6 +41,28 @@ test.describe("Org", () => {
|
|||
await expectPageToBeServerSideRendered(page);
|
||||
});
|
||||
});
|
||||
test.describe("Dynamic Group Booking", () => {
|
||||
test("Dynamic Group booking link should load", async ({ page }) => {
|
||||
const users = [
|
||||
{
|
||||
username: "peer",
|
||||
name: "Peer Richelsen",
|
||||
},
|
||||
{
|
||||
username: "bailey",
|
||||
name: "Bailey Pumfleet",
|
||||
},
|
||||
];
|
||||
const response = await page.goto(`http://i.cal.com/${users[0].username}+${users[1].username}`);
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(await page.locator('[data-testid="event-title"]').textContent()).toBe("Dynamic");
|
||||
|
||||
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[0].name);
|
||||
expect(await page.locator('[data-testid="event-meta"]').textContent()).toContain(users[1].name);
|
||||
// 2 users and 1 for the organization(2+1)
|
||||
expect((await page.locator('[data-testid="event-meta"] [data-testid="avatar"]').all()).length).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// This ensures that the route is actually mapped to a page that is using withEmbedSsr
|
||||
|
|
|
@ -185,7 +185,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
|||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
organization: userOrgQuery(context.req.headers.host ?? "", context.params?.orgSlug),
|
||||
organization: userOrgQuery(context.req, context.params?.orgSlug),
|
||||
},
|
||||
select: {
|
||||
away: true,
|
||||
|
|
|
@ -14,15 +14,15 @@ import TeamTypePage, { getServerSideProps as GSSTeamTypePage } from "../../../te
|
|||
|
||||
const paramsSchema = z.object({
|
||||
orgSlug: z.string().transform((s) => slugify(s)),
|
||||
user: z.string().transform((s) => slugify(s)),
|
||||
user: z.string(),
|
||||
type: z.string().transform((s) => slugify(s)),
|
||||
});
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
const { user: teamOrUserSlug, orgSlug, type } = paramsSchema.parse(ctx.params);
|
||||
const { user: teamOrUserSlugOrDynamicGroup, orgSlug, type } = paramsSchema.parse(ctx.params);
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: teamOrUserSlug,
|
||||
slug: slugify(teamOrUserSlugOrDynamicGroup),
|
||||
parentId: {
|
||||
not: null,
|
||||
},
|
||||
|
@ -34,7 +34,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||
});
|
||||
|
||||
if (team) {
|
||||
const params = { slug: teamOrUserSlug, type };
|
||||
const params = { slug: teamOrUserSlugOrDynamicGroup, type };
|
||||
return GSSTeamTypePage({
|
||||
...ctx,
|
||||
params: {
|
||||
|
@ -47,7 +47,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
|||
},
|
||||
});
|
||||
}
|
||||
const params = { user: teamOrUserSlug, type };
|
||||
const params = { user: teamOrUserSlugOrDynamicGroup, type };
|
||||
return GSSUserTypePage({
|
||||
...ctx,
|
||||
params: {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { expect } from "@playwright/test";
|
||||
|
||||
import { MembershipRole } from "@calcom/prisma/client";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import {
|
||||
bookTimeSlot,
|
||||
doOnOrgDomain,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
selectSecondAvailableTimeSlotNextMonth,
|
||||
} from "./lib/testUtils";
|
||||
|
@ -58,3 +61,46 @@ test("dynamic booking", async ({ page, users }) => {
|
|||
await expect(cancelledHeadline).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Organization:", () => {
|
||||
test.afterEach(({ orgs, users }) => {
|
||||
orgs.deleteAll();
|
||||
users.deleteAll();
|
||||
});
|
||||
test("Can book a time slot for an organization", async ({ page, users, orgs }) => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
});
|
||||
|
||||
const user1 = await users.create({
|
||||
organizationId: org.id,
|
||||
name: "User 1",
|
||||
roleInOrganization: MembershipRole.ADMIN,
|
||||
});
|
||||
|
||||
const user2 = await users.create({
|
||||
organizationId: org.id,
|
||||
name: "User 2",
|
||||
roleInOrganization: MembershipRole.ADMIN,
|
||||
});
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async () => {
|
||||
await page.goto(`/${user1.username}+${user2.username}`);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page, {
|
||||
title: "Test meeting",
|
||||
});
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
// All the teammates should be in the booking
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await expect(page.getByText(user1.name!, { exact: true })).toBeVisible();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await expect(page.getByText(user2.name!, { exact: true })).toBeVisible();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -134,12 +134,16 @@ export async function bookFirstEvent(page: Page) {
|
|||
await bookEventOnThisPage(page);
|
||||
}
|
||||
|
||||
export const bookTimeSlot = async (page: Page, opts?: { name?: string; email?: string }) => {
|
||||
export const bookTimeSlot = async (page: Page, opts?: { name?: string; email?: string; title?: string }) => {
|
||||
// --- fill form
|
||||
await page.fill('[name="name"]', opts?.name ?? testName);
|
||||
await page.fill('[name="email"]', opts?.email ?? testEmail);
|
||||
if (opts?.title) {
|
||||
await page.fill('[name="title"]', opts.title);
|
||||
}
|
||||
await page.press('[name="email"]', "Enter");
|
||||
};
|
||||
|
||||
// Provide an standalone localize utility not managed by next-i18n
|
||||
export async function localize(locale: string) {
|
||||
const localeModule = `../../public/static/locales/${locale}/common.json`;
|
||||
|
@ -337,6 +341,19 @@ export async function fillStripeTestCheckout(page: Page) {
|
|||
await page.click(".SubmitButton--complete-Shimmer");
|
||||
}
|
||||
|
||||
export async function doOnOrgDomain(
|
||||
{ orgSlug, page }: { orgSlug: string | null; page: Page },
|
||||
callback: ({ page }: { page: Page }) => Promise<void>
|
||||
) {
|
||||
if (!orgSlug) {
|
||||
throw new Error("orgSlug is not available");
|
||||
}
|
||||
page.setExtraHTTPHeaders({
|
||||
"x-cal-force-slug": orgSlug,
|
||||
});
|
||||
await callback({ page });
|
||||
}
|
||||
|
||||
// When App directory is there, this is the 404 page text. It is commented till it's disabled
|
||||
// export const NotFoundPageText = "This page could not be found";
|
||||
export const NotFoundPageText = "ERROR 404";
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
|
@ -9,6 +8,7 @@ import { test } from "./lib/fixtures";
|
|||
import {
|
||||
NotFoundPageText,
|
||||
bookTimeSlot,
|
||||
doOnOrgDomain,
|
||||
fillStripeTestCheckout,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
testName,
|
||||
|
@ -459,16 +459,3 @@ test.describe("Teams - Org", () => {
|
|||
await page.waitForSelector("[data-testid=day]");
|
||||
});
|
||||
});
|
||||
|
||||
async function doOnOrgDomain(
|
||||
{ orgSlug, page }: { orgSlug: string | null; page: Page },
|
||||
callback: ({ page }: { page: Page }) => Promise<void>
|
||||
) {
|
||||
if (!orgSlug) {
|
||||
throw new Error("orgSlug is not available");
|
||||
}
|
||||
page.setExtraHTTPHeaders({
|
||||
"x-cal-force-slug": orgSlug,
|
||||
});
|
||||
await callback({ page });
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ export const EventMeta = () => {
|
|||
: "text-bookinghighlight";
|
||||
|
||||
return (
|
||||
<div className="relative z-10 p-6">
|
||||
<div className="relative z-10 p-6" data-testid="event-meta">
|
||||
{isLoading && (
|
||||
<m.div {...fadeInUp} initial="visible" layout>
|
||||
<EventMetaSkeleton />
|
||||
|
|
|
@ -11,5 +11,9 @@ interface EventTitleProps {
|
|||
|
||||
export const EventTitle = ({ children, as, className }: EventTitleProps) => {
|
||||
const El = as || "h1";
|
||||
return <El className={classNames("text-text text-xl font-semibold", className)}>{children}</El>;
|
||||
return (
|
||||
<El data-testid="event-title" className={classNames("text-text text-xl font-semibold", className)}>
|
||||
{children}
|
||||
</El>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { App, Attendee, DestinationCalendar, EventTypeCustomInput } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import async from "async";
|
||||
import type { IncomingMessage } from "http";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { cloneDeep } from "lodash";
|
||||
|
@ -372,21 +373,16 @@ type IsFixedAwareUser = User & {
|
|||
organization: { slug: string };
|
||||
};
|
||||
|
||||
const loadUsers = async (
|
||||
eventType: NewBookingEventType,
|
||||
dynamicUserList: string[],
|
||||
reqHeadersHost: string | undefined
|
||||
) => {
|
||||
const loadUsers = async (eventType: NewBookingEventType, dynamicUserList: string[], req: IncomingMessage) => {
|
||||
try {
|
||||
if (!eventType.id) {
|
||||
if (!Array.isArray(dynamicUserList) || dynamicUserList.length === 0) {
|
||||
throw new Error("dynamicUserList is not properly defined or empty.");
|
||||
}
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
username: { in: dynamicUserList },
|
||||
organization: userOrgQuery(reqHeadersHost ? reqHeadersHost.replace(/^https?:\/\//, "") : ""),
|
||||
organization: userOrgQuery(req),
|
||||
},
|
||||
select: {
|
||||
...userSelect.select,
|
||||
|
@ -969,7 +965,7 @@ async function handler(
|
|||
let users: (Awaited<ReturnType<typeof loadUsers>>[number] & {
|
||||
isFixed?: boolean;
|
||||
metadata?: Prisma.JsonValue;
|
||||
})[] = await loadUsers(eventType, dynamicUserList, req.headers.host);
|
||||
})[] = await loadUsers(eventType, dynamicUserList, req);
|
||||
|
||||
const isDynamicAllowed = !users.some((user) => !user.allowDynamicBooking);
|
||||
if (!isDynamicAllowed && !eventTypeId) {
|
||||
|
|
|
@ -145,7 +145,7 @@ export function whereClauseForOrgWithSlugOrRequestedSlug(slug: string) {
|
|||
} satisfies Prisma.TeamWhereInput;
|
||||
}
|
||||
|
||||
export function userOrgQuery(hostname: string, fallback?: string | string[]) {
|
||||
const { currentOrgDomain, isValidOrgDomain } = getOrgDomainConfigFromHostname({ hostname, fallback });
|
||||
export function userOrgQuery(req: IncomingMessage | undefined, fallback?: string | string[]) {
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req, fallback);
|
||||
return isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
|
|||
{displayedAvatars.map((item, idx) => (
|
||||
<li key={idx} className="-mr-[4px] inline-block">
|
||||
<Avatar
|
||||
data-testid="avatar"
|
||||
className="border-subtle"
|
||||
imageSrc={item.image}
|
||||
title={item.title}
|
||||
|
|
Loading…
Reference in New Issue
Block a user