fix: add default schedule while creating org member (#16855)

* add default schedule while creating org user

* naming as per review comments

* added e2e test

* move bookTeamEvent and expectPageNotFound to testUtils

* Update EventTeamAssignmentTab.tsx

* Update AddMembersWithSwitch.tsx

* Update utils.ts

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
This commit is contained in:
Vijay
2024-11-18 20:03:24 +05:30
committed by GitHub
co-authored by Anik Dhabal Babu
parent 9aa8f8a30c
commit 7b94822c8d
6 changed files with 124 additions and 51 deletions
+50 -1
View File
@@ -9,7 +9,7 @@ import type { Messages } from "mailhog";
import { totp } from "otplib";
import type { Prisma } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
import type { IntervalLimit } from "@calcom/types/Calendar";
import type { createEmailsFixture } from "../fixtures/emails";
@@ -464,3 +464,52 @@ export async function confirmReschedule(page: Page, url = "/api/book/event") {
action: () => page.locator('[data-testid="confirm-reschedule-button"]').click(),
});
}
export async function bookTeamEvent({
page,
team,
event,
teamMatesObj,
opts,
}: {
page: Page;
team: {
slug: string | null;
name: string | null;
};
event: { slug: string; title: string; schedulingType: SchedulingType | null };
teamMatesObj?: { name: string }[];
opts?: { attendeePhoneNumber?: string };
}) {
// Note that even though the default way to access a team booking in an organization is to not use /team in the URL, but it isn't testable with playwright as the rewrite is taken care of by Next.js config which can't handle on the fly org slug's handling
// So, we are using /team in the URL to access the team booking
// There are separate tests to verify that the next.config.js rewrites are working
// Also there are additional checkly tests that verify absolute e2e flow. They are in __checks__/organization.spec.ts
await page.goto(`/team/${team.slug}/${event.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page, opts);
await expect(page.getByTestId("success-page")).toBeVisible();
// The title of the booking
if (event.schedulingType === SchedulingType.ROUND_ROBIN && teamMatesObj) {
const bookingTitle = await page.getByTestId("booking-title").textContent();
const isMatch = teamMatesObj?.some((teamMate) => {
const expectedTitle = `${event.title} between ${teamMate.name} and ${testName}`;
return expectedTitle.trim() === bookingTitle?.trim();
});
expect(isMatch).toBe(true);
} else {
const BookingTitle = `${event.title} between ${team.name} and ${testName}`;
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
}
// The booker should be in the attendee list
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
}
export async function expectPageToBeNotFound({ page, url }: { page: Page; url: string }) {
await page.goto(`${url}`);
await expect(page.getByTestId(`404-page`)).toBeVisible();
}
@@ -9,8 +9,10 @@ import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { test } from "../lib/fixtures";
import {
bookTeamEvent,
bookTimeSlot,
doOnOrgDomain,
expectPageToBeNotFound,
selectFirstAvailableTimeSlotNextMonth,
submitAndWaitForResponse,
testName,
@@ -604,55 +606,6 @@ async function bookUserEvent({
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
}
async function bookTeamEvent({
page,
team,
event,
teamMatesObj,
opts,
}: {
page: Page;
team: {
slug: string | null;
name: string | null;
};
event: { slug: string; title: string; schedulingType: SchedulingType | null };
teamMatesObj?: { name: string }[];
opts?: { attendeePhoneNumber?: string };
}) {
// Note that even though the default way to access a team booking in an organization is to not use /team in the URL, but it isn't testable with playwright as the rewrite is taken care of by Next.js config which can't handle on the fly org slug's handling
// So, we are using /team in the URL to access the team booking
// There are separate tests to verify that the next.config.js rewrites are working
// Also there are additional checkly tests that verify absolute e2e flow. They are in __checks__/organization.spec.ts
await page.goto(`/team/${team.slug}/${event.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page, opts);
await expect(page.getByTestId("success-page")).toBeVisible();
// The title of the booking
if (event.schedulingType === SchedulingType.ROUND_ROBIN && teamMatesObj) {
const bookingTitle = await page.getByTestId("booking-title").textContent();
const isMatch = teamMatesObj?.some((teamMate) => {
const expectedTitle = `${event.title} between ${teamMate.name} and ${testName}`;
return expectedTitle.trim() === bookingTitle?.trim();
});
expect(isMatch).toBe(true);
} else {
const BookingTitle = `${event.title} between ${team.name} and ${testName}`;
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
}
// The booker should be in the attendee list
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
}
async function expectPageToBeNotFound({ page, url }: { page: Page; url: string }) {
await page.goto(`${url}`);
await expect(page.getByTestId(`404-page`)).toBeVisible();
}
const markPhoneNumberAsRequiredAndEmailAsOptional = async (page: Page, eventId: number) => {
// Make phone as required
await markPhoneNumberAsRequiredField(page, eventId);
@@ -3,11 +3,12 @@ import { expect } from "@playwright/test";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/client";
import { SchedulingType } from "@calcom/prisma/enums";
import { moveUserToOrg } from "@lib/orgMigration";
import { test } from "../lib/fixtures";
import { getInviteLink } from "../lib/testUtils";
import { bookTeamEvent, doOnOrgDomain, expectPageToBeNotFound, getInviteLink } from "../lib/testUtils";
import { expectInvitationEmailToBeReceived } from "./expects";
test.describe.configure({ mode: "parallel" });
@@ -417,6 +418,51 @@ test.describe("Organization", () => {
});
});
});
test("can book an event with auto accepted invitee (not completed on-boarding) added as fixed host.", async ({
page,
users,
}) => {
const orgOwner = await users.create(undefined, {
hasTeam: true,
isOrg: true,
hasSubteam: true,
isOrgVerified: true,
isDnsSetup: true,
orgRequestedSlug: "example",
schedulingType: SchedulingType.ROUND_ROBIN,
});
const { team: org } = await orgOwner.getOrgMembership();
const { team } = await orgOwner.getFirstTeamMembership();
await orgOwner.apiLogin();
await page.goto(`/settings/teams/${team.id}/members`);
const invitedUserEmail = users.trackEmail({ username: "rick", domain: "example.com" });
await inviteAnEmail(page, invitedUserEmail, true);
//add invitee as fixed host to team event
const teamEvent = await orgOwner.getFirstTeamEvent(team.id);
await page.goto(`/event-types/${teamEvent.id}?tabName=team`);
await page.locator('[data-testid="fixed-hosts-switch"]').click();
await page.locator('[data-testid="fixed-hosts-select"]').click();
await page.locator(`text="${invitedUserEmail}"`).click();
await page.locator('[data-testid="update-eventtype"]').click();
await page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
await expectPageToBeNotFound({ page, url: `/team/${team.slug}/${teamEvent.slug}` });
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async ({ page, goToUrlWithErrorHandling }) => {
const result = await goToUrlWithErrorHandling(`/team/${team.slug}/${teamEvent.slug}`);
await bookTeamEvent({ page, team, event: teamEvent });
await expect(page.getByText(invitedUserEmail, { exact: true })).toBeVisible();
return { url: result.url };
}
);
});
});
});
@@ -111,6 +111,7 @@ const AddMembersWithSwitch = ({
placeholder = "",
containerClassName = "",
isRRWeightsEnabled,
...rest
}: {
value: Host[];
onChange: (hosts: Host[]) => void;
@@ -123,6 +124,7 @@ const AddMembersWithSwitch = ({
placeholder?: string;
containerClassName?: string;
isRRWeightsEnabled?: boolean;
"data-testid"?: string;
}) => {
const { t } = useLocale();
const { setValue } = useFormContext<FormValues>();
@@ -144,6 +146,7 @@ const AddMembersWithSwitch = ({
)}
{!assignAllTeamMembers || !automaticAddAllEnabled ? (
<CheckedHostField
data-testid={rest["data-testid"]}
value={value}
onChange={onChange}
isFixed={isFixed}
@@ -157,6 +157,7 @@ const FixedHosts = ({
</>
) : (
<SettingsToggle
data-testid="fixed-hosts-switch"
toggleSwitchAtTheEnd={true}
title={t("fixed_hosts")}
description={FixedHostHelper}
@@ -175,6 +176,7 @@ const FixedHosts = ({
childrenClassName="lg:ml-0">
<div className="border-subtle flex flex-col gap-6 rounded-bl-md rounded-br-md border border-t-0 px-6">
<AddMembersWithSwitch
data-testid="fixed-hosts-select"
teamMembers={teamMembers}
value={value}
onChange={onChange}
@@ -3,6 +3,7 @@ import type { TFunction } from "next-i18next";
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import { sendTeamInviteEmail } from "@calcom/emails";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { ENABLE_PROFILE_SWITCHER, WEBAPP_URL } from "@calcom/lib/constants";
import { createAProfileForAnExistingUser } from "@calcom/lib/createAProfileForAnExistingUser";
import logger from "@calcom/lib/logger";
@@ -272,6 +273,7 @@ export async function createNewUsersConnectToOrgIfExists({
timeFormat,
weekStart,
timeZone,
language,
}: {
invitations: Invitation[];
isOrg: boolean;
@@ -283,6 +285,7 @@ export async function createNewUsersConnectToOrgIfExists({
timeFormat?: number;
weekStart?: string;
timeZone?: string;
language: string;
}) {
// fail if we have invalid emails
invitations.forEach((invitation) => checkInputEmailIsValid(invitation.usernameOrEmail));
@@ -307,6 +310,8 @@ export async function createNewUsersConnectToOrgIfExists({
const isBecomingAnOrgMember = parentId || isOrg;
const defaultAvailability = getAvailabilityFromSchedule(DEFAULT_SCHEDULE);
const t = await getTranslation(language ?? "en", "common");
const createdUser = await tx.user.create({
data: {
username: isBecomingAnOrgMember ? orgMemberUsername : regularTeamMemberUsername,
@@ -340,6 +345,20 @@ export async function createNewUsersConnectToOrgIfExists({
accepted: autoAccept, // If the user is invited to a child team, they are automatically accepted
},
},
schedules: {
create: {
name: t("default_schedule_name"),
availability: {
createMany: {
data: defaultAvailability.map((schedule) => ({
days: schedule.days,
startTime: schedule.startTime,
endTime: schedule.endTime,
})),
},
},
},
},
},
});
@@ -908,6 +927,7 @@ export async function handleNewUsersInvites({
orgConnectInfoByUsernameOrEmail,
autoAcceptEmailDomain: autoAcceptEmailDomain,
parentId: team.parentId,
language,
});
const sendVerifyEmailsPromises = invitationsForNewUsers.map((invitation) => {