From 37547335a3257f6887e53a2754958f659ce7cddb Mon Sep 17 00:00:00 2001 From: Vijay Date: Fri, 27 Sep 2024 13:50:45 +0530 Subject: [PATCH 1/9] add default schedule while creating org user --- .../viewer/teams/inviteMember/utils.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts index 3c8fad0f24b109..61a0a7a945a576 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts @@ -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 availability = getAvailabilityFromSchedule(DEFAULT_SCHEDULE); + const t = await getTranslation(language, "common"); const createdUser = await tx.user.create({ data: { username: isBecomingAnOrgMember ? orgMemberUsername : regularTeamMemberUsername, @@ -340,6 +345,21 @@ export async function createNewUsersConnectToOrgIfExists({ accepted: autoAccept, // If the user is invited to a child team, they are automatically accepted }, }, + // Default schedule + schedules: { + create: { + name: t("default_schedule_name"), + availability: { + createMany: { + data: availability.map((schedule) => ({ + days: schedule.days, + startTime: schedule.startTime, + endTime: schedule.endTime, + })), + }, + }, + }, + }, }, }); @@ -908,6 +928,7 @@ export async function handleNewUsersInvites({ orgConnectInfoByUsernameOrEmail, autoAcceptEmailDomain: autoAcceptEmailDomain, parentId: team.parentId, + language, }); const sendVerifyEmailsPromises = invitationsForNewUsers.map((invitation) => { From 66870007f1167af0a133786b1ddca4df562939e9 Mon Sep 17 00:00:00 2001 From: Vijay Date: Sun, 29 Sep 2024 17:00:21 +0530 Subject: [PATCH 2/9] naming as per review comments --- .../trpc/server/routers/viewer/teams/inviteMember/utils.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts index 61a0a7a945a576..e564521fa6423c 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts @@ -310,7 +310,7 @@ export async function createNewUsersConnectToOrgIfExists({ const isBecomingAnOrgMember = parentId || isOrg; - const availability = getAvailabilityFromSchedule(DEFAULT_SCHEDULE); + const defaultAvailability = getAvailabilityFromSchedule(DEFAULT_SCHEDULE); const t = await getTranslation(language, "common"); const createdUser = await tx.user.create({ data: { @@ -345,13 +345,12 @@ export async function createNewUsersConnectToOrgIfExists({ accepted: autoAccept, // If the user is invited to a child team, they are automatically accepted }, }, - // Default schedule schedules: { create: { name: t("default_schedule_name"), availability: { createMany: { - data: availability.map((schedule) => ({ + data: defaultAvailability.map((schedule) => ({ days: schedule.days, startTime: schedule.startTime, endTime: schedule.endTime, From 8022fae0fe1d5631639931e68de7ee8e23203394 Mon Sep 17 00:00:00 2001 From: Vijay Date: Fri, 8 Nov 2024 18:44:02 +0530 Subject: [PATCH 3/9] added e2e test --- .../playwright/organization/booking.e2e.ts | 4 +- .../organization-invitation.e2e.ts | 49 ++++++++++++++++++- .../components/AddMembersWithSwitch.tsx | 3 ++ .../assignment/EventTeamAssignmentTab.tsx | 2 + 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/apps/web/playwright/organization/booking.e2e.ts b/apps/web/playwright/organization/booking.e2e.ts index 1b2903a6eafd8e..c949886d392f84 100644 --- a/apps/web/playwright/organization/booking.e2e.ts +++ b/apps/web/playwright/organization/booking.e2e.ts @@ -604,7 +604,7 @@ async function bookUserEvent({ await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName); } -async function bookTeamEvent({ +export async function bookTeamEvent({ page, team, event, @@ -648,7 +648,7 @@ async function bookTeamEvent({ await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName); } -async function expectPageToBeNotFound({ page, url }: { page: Page; url: string }) { +export async function expectPageToBeNotFound({ page, url }: { page: Page; url: string }) { await page.goto(`${url}`); await expect(page.getByTestId(`404-page`)).toBeVisible(); } diff --git a/apps/web/playwright/organization/organization-invitation.e2e.ts b/apps/web/playwright/organization/organization-invitation.e2e.ts index 21333d12a2720d..feab39c7993549 100644 --- a/apps/web/playwright/organization/organization-invitation.e2e.ts +++ b/apps/web/playwright/organization/organization-invitation.e2e.ts @@ -3,11 +3,13 @@ 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 { doOnOrgDomain, getInviteLink } from "../lib/testUtils"; +import { bookTeamEvent, expectPageToBeNotFound } from "./booking.e2e"; import { expectInvitationEmailToBeReceived } from "./expects"; test.describe.configure({ mode: "parallel" }); @@ -417,6 +419,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 }; + } + ); + }); }); }); diff --git a/packages/features/eventtypes/components/AddMembersWithSwitch.tsx b/packages/features/eventtypes/components/AddMembersWithSwitch.tsx index 4a136f766861bd..1fcae7f35a0657 100644 --- a/packages/features/eventtypes/components/AddMembersWithSwitch.tsx +++ b/packages/features/eventtypes/components/AddMembersWithSwitch.tsx @@ -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(); @@ -144,6 +146,7 @@ const AddMembersWithSwitch = ({ )} {!assignAllTeamMembers || !automaticAddAllEnabled ? ( ) : (
Date: Mon, 11 Nov 2024 12:36:35 +0530 Subject: [PATCH 4/9] move bookTeamEvent and expectPageNotFound to testUtils --- apps/web/playwright/lib/testUtils.ts | 51 ++++++++++++++++++- .../playwright/organization/booking.e2e.ts | 51 +------------------ .../organization-invitation.e2e.ts | 3 +- 3 files changed, 53 insertions(+), 52 deletions(-) diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts index 39d49ee8b21963..15fd6be921fe48 100644 --- a/apps/web/playwright/lib/testUtils.ts +++ b/apps/web/playwright/lib/testUtils.ts @@ -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(); +} diff --git a/apps/web/playwright/organization/booking.e2e.ts b/apps/web/playwright/organization/booking.e2e.ts index c949886d392f84..c0a8eca4f40684 100644 --- a/apps/web/playwright/organization/booking.e2e.ts +++ b/apps/web/playwright/organization/booking.e2e.ts @@ -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); } -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(); -} - const markPhoneNumberAsRequiredAndEmailAsOptional = async (page: Page, eventId: number) => { // Make phone as required await markPhoneNumberAsRequiredField(page, eventId); diff --git a/apps/web/playwright/organization/organization-invitation.e2e.ts b/apps/web/playwright/organization/organization-invitation.e2e.ts index feab39c7993549..881e4f426abb5e 100644 --- a/apps/web/playwright/organization/organization-invitation.e2e.ts +++ b/apps/web/playwright/organization/organization-invitation.e2e.ts @@ -8,8 +8,7 @@ import { SchedulingType } from "@calcom/prisma/enums"; import { moveUserToOrg } from "@lib/orgMigration"; import { test } from "../lib/fixtures"; -import { doOnOrgDomain, getInviteLink } from "../lib/testUtils"; -import { bookTeamEvent, expectPageToBeNotFound } from "./booking.e2e"; +import { bookTeamEvent, doOnOrgDomain, expectPageToBeNotFound, getInviteLink } from "../lib/testUtils"; import { expectInvitationEmailToBeReceived } from "./expects"; test.describe.configure({ mode: "parallel" }); From 122ea4f7c5c73c905a549f375aa08e811fc4e07e Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Mon, 18 Nov 2024 18:30:34 +0530 Subject: [PATCH 5/9] Update EventTeamAssignmentTab.tsx --- .../components/tabs/assignment/EventTeamAssignmentTab.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx b/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx index ffb431e9519bc5..d0721b7836ae78 100644 --- a/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx +++ b/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx @@ -176,7 +176,6 @@ const FixedHosts = ({ childrenClassName="lg:ml-0">
Date: Mon, 18 Nov 2024 18:32:10 +0530 Subject: [PATCH 6/9] Update AddMembersWithSwitch.tsx --- .../features/eventtypes/components/AddMembersWithSwitch.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/features/eventtypes/components/AddMembersWithSwitch.tsx b/packages/features/eventtypes/components/AddMembersWithSwitch.tsx index 1fcae7f35a0657..232ce090b86b97 100644 --- a/packages/features/eventtypes/components/AddMembersWithSwitch.tsx +++ b/packages/features/eventtypes/components/AddMembersWithSwitch.tsx @@ -111,7 +111,6 @@ const AddMembersWithSwitch = ({ placeholder = "", containerClassName = "", isRRWeightsEnabled, - ...rest }: { value: Host[]; onChange: (hosts: Host[]) => void; @@ -124,7 +123,6 @@ const AddMembersWithSwitch = ({ placeholder?: string; containerClassName?: string; isRRWeightsEnabled?: boolean; - "data-testid"?: string; }) => { const { t } = useLocale(); const { setValue } = useFormContext(); @@ -146,7 +144,7 @@ const AddMembersWithSwitch = ({ )} {!assignAllTeamMembers || !automaticAddAllEnabled ? ( Date: Mon, 18 Nov 2024 18:33:37 +0530 Subject: [PATCH 7/9] Update utils.ts --- packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts index e564521fa6423c..d63d872c46a14e 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts @@ -311,7 +311,7 @@ export async function createNewUsersConnectToOrgIfExists({ const isBecomingAnOrgMember = parentId || isOrg; const defaultAvailability = getAvailabilityFromSchedule(DEFAULT_SCHEDULE); - const t = await getTranslation(language, "common"); + const t = await getTranslation(language ?? "en", "common"); const createdUser = await tx.user.create({ data: { username: isBecomingAnOrgMember ? orgMemberUsername : regularTeamMemberUsername, From 8127ed1e41f9aed97f31acdb9b61ddd100aa8a98 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 18 Nov 2024 19:07:37 +0530 Subject: [PATCH 8/9] update --- packages/features/eventtypes/components/AddMembersWithSwitch.tsx | 1 - packages/features/eventtypes/components/CheckedTeamSelect.tsx | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/features/eventtypes/components/AddMembersWithSwitch.tsx b/packages/features/eventtypes/components/AddMembersWithSwitch.tsx index 232ce090b86b97..4a136f766861bd 100644 --- a/packages/features/eventtypes/components/AddMembersWithSwitch.tsx +++ b/packages/features/eventtypes/components/AddMembersWithSwitch.tsx @@ -144,7 +144,6 @@ const AddMembersWithSwitch = ({ )} {!assignAllTeamMembers || !automaticAddAllEnabled ? ( {/* This class name conditional looks a bit odd but it allows a seemless transition when using autoanimate From 160ddf78c1f61fce2113298cec11c56945e6dafa Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 18 Nov 2024 19:42:13 +0530 Subject: [PATCH 9/9] revert --- .../features/eventtypes/components/AddMembersWithSwitch.tsx | 3 +++ packages/features/eventtypes/components/CheckedTeamSelect.tsx | 1 - .../components/tabs/assignment/EventTeamAssignmentTab.tsx | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/features/eventtypes/components/AddMembersWithSwitch.tsx b/packages/features/eventtypes/components/AddMembersWithSwitch.tsx index 4a136f766861bd..1fcae7f35a0657 100644 --- a/packages/features/eventtypes/components/AddMembersWithSwitch.tsx +++ b/packages/features/eventtypes/components/AddMembersWithSwitch.tsx @@ -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(); @@ -144,6 +146,7 @@ const AddMembersWithSwitch = ({ )} {!assignAllTeamMembers || !automaticAddAllEnabled ? ( {/* This class name conditional looks a bit odd but it allows a seemless transition when using autoanimate diff --git a/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx b/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx index d0721b7836ae78..ffb431e9519bc5 100644 --- a/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx +++ b/packages/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab.tsx @@ -176,6 +176,7 @@ const FixedHosts = ({ childrenClassName="lg:ml-0">