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:
co-authored by
Anik Dhabal Babu
parent
9aa8f8a30c
commit
7b94822c8d
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user