Merge branch 'main' into 12544-cal-2758-left-align-profile-view

This commit is contained in:
Peer Richelsen 2023-11-27 22:00:48 +00:00 committed by GitHub
commit acab4fd945
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 2385 additions and 1511 deletions

View File

@ -1,19 +1,337 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { Linter } from "eslint";
import { parse } from "node-html-parser";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { MembershipRole } from "@calcom/prisma/client";
import { test } from "./lib/fixtures";
function chooseEmbedType(page: Page, embedType: string) {
const linter = new Linter();
const eslintRules = {
"no-undef": "error",
"no-unused-vars": "off",
} as const;
test.describe.configure({ mode: "parallel" });
test.afterEach(({ users }) => users.deleteAll());
test.describe("Embed Code Generator Tests", () => {
test.describe("Non-Organization", () => {
test.beforeEach(async ({ users }) => {
const pro = await users.create();
await pro.apiLogin();
});
test.describe("Event Types Page", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/event-types");
});
test("open Embed Dialog and choose Inline for First Event Type", async ({ page, users }) => {
const [pro] = users.get();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "inline");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "inline",
basePage: "/event-types",
});
await expectToContainValidCode(page, {
language: "html",
embedType: "inline",
orgSlug: null,
});
await goToReactCodeTab(page);
await expectToContainValidCode(page, {
language: "react",
embedType: "inline",
orgSlug: null,
});
await goToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "inline",
calLink: `${pro.username}/30-min`,
});
});
test("open Embed Dialog and choose floating-popup for First Event Type", async ({ page, users }) => {
const [pro] = users.get();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "floating-popup");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "floating-popup",
basePage: "/event-types",
});
await expectToContainValidCode(page, {
language: "html",
embedType: "floating-popup",
orgSlug: null,
});
await goToReactCodeTab(page);
await expectToContainValidCode(page, {
language: "react",
embedType: "floating-popup",
orgSlug: null,
});
await goToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "floating-popup",
calLink: `${pro.username}/30-min`,
});
});
test("open Embed Dialog and choose element-click for First Event Type", async ({ page, users }) => {
const [pro] = users.get();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "element-click");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "element-click",
basePage: "/event-types",
});
await expectToContainValidCode(page, {
language: "html",
embedType: "element-click",
orgSlug: null,
});
await goToReactCodeTab(page);
await expectToContainValidCode(page, {
language: "react",
embedType: "element-click",
orgSlug: null,
});
await goToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "element-click",
calLink: `${pro.username}/30-min`,
});
});
});
test.describe("Event Type Edit Page", () => {
test.beforeEach(async ({ page }) => {
await page.goto(`/event-types`);
await Promise.all([
page.locator('a[href*="/event-types/"]').first().click(),
page.waitForURL((url) => url.pathname.startsWith("/event-types/")),
]);
});
test("open Embed Dialog for the Event Type", async ({ page }) => {
const basePage = new URL(page.url()).pathname;
const embedUrl = await clickEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage,
});
chooseEmbedType(page, "inline");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
basePage,
embedType: "inline",
});
await expectToContainValidCode(page, {
language: "html",
embedType: "inline",
orgSlug: null,
});
await goToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "inline",
calLink: decodeURIComponent(embedUrl),
});
});
});
});
test.describe("Organization", () => {
test.beforeEach(async ({ users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user = await users.create({
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
await user.apiLogin();
});
test.describe("Event Types Page", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/event-types");
});
test("open Embed Dialog and choose Inline for First Event Type", async ({ page, users }) => {
const [user] = users.get();
const { team: org } = await user.getOrgMembership();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "inline");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "inline",
basePage: "/event-types",
});
// Default tab is HTML code tab
await expectToContainValidCode(page, {
language: "html",
embedType: "inline",
orgSlug: org.slug,
});
await goToReactCodeTab(page);
await expectToContainValidCode(page, {
language: "react",
embedType: "inline",
orgSlug: org.slug,
});
await goToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "inline",
calLink: `${user.username}/30-min`,
bookerUrl: getOrgFullOrigin(org?.slug ?? ""),
});
});
test("open Embed Dialog and choose floating-popup for First Event Type", async ({ page, users }) => {
const [user] = users.get();
const { team: org } = await user.getOrgMembership();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "floating-popup");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "floating-popup",
basePage: "/event-types",
});
await expectToContainValidCode(page, {
language: "html",
embedType: "floating-popup",
orgSlug: org.slug,
});
await goToReactCodeTab(page);
await expectToContainValidCode(page, {
language: "react",
embedType: "floating-popup",
orgSlug: org.slug,
});
await goToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "floating-popup",
calLink: `${user.username}/30-min`,
bookerUrl: getOrgFullOrigin(org?.slug ?? ""),
});
});
test("open Embed Dialog and choose element-click for First Event Type", async ({ page, users }) => {
const [user] = users.get();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
const { team: org } = await user.getOrgMembership();
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "element-click");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "element-click",
basePage: "/event-types",
});
await expectToContainValidCode(page, {
language: "html",
embedType: "element-click",
orgSlug: org.slug,
});
await goToReactCodeTab(page);
await expectToContainValidCode(page, {
language: "react",
embedType: "element-click",
orgSlug: org.slug,
});
await goToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "element-click",
calLink: `${user.username}/30-min`,
bookerUrl: getOrgFullOrigin(org?.slug ?? ""),
});
});
});
});
});
type EmbedType = "inline" | "floating-popup" | "element-click";
function chooseEmbedType(page: Page, embedType: EmbedType) {
page.locator(`[data-testid=${embedType}]`).click();
}
async function gotToPreviewTab(page: Page) {
async function goToPreviewTab(page: Page) {
// To prevent early timeouts
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
await page.locator("[data-testid=embed-tabs]").locator("text=Preview").click();
await page.locator("[data-testid=horizontal-tab-Preview]").click();
}
async function goToReactCodeTab(page: Page) {
// To prevent early timeouts
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
await page.locator("[data-testid=horizontal-tab-React]").click();
}
async function clickEmbedButton(page: Page) {
@ -55,7 +373,7 @@ async function expectToBeNavigatingToEmbedCodeAndPreviewDialog(
basePage,
}: {
embedUrl: string | null;
embedType: string;
embedType: EmbedType;
basePage: string;
}
) {
@ -73,10 +391,108 @@ async function expectToBeNavigatingToEmbedCodeAndPreviewDialog(
});
}
async function expectToContainValidCode(page: Page, { embedType }: { embedType: string }) {
async function expectToContainValidCode(
page: Page,
{
embedType,
language,
orgSlug,
}: { embedType: EmbedType; language: "html" | "react"; orgSlug: string | null }
) {
if (language === "react") {
return expectValidReactEmbedSnippet(page, { embedType, orgSlug });
}
if (language === "html") {
return expectValidHtmlEmbedSnippet(page, { embedType, orgSlug });
}
throw new Error("Unknown language");
}
async function expectValidHtmlEmbedSnippet(
page: Page,
{ embedType, orgSlug }: { embedType: EmbedType; orgSlug: string | null }
) {
const embedCode = await page.locator("[data-testid=embed-code]").inputValue();
expect(embedCode.includes("(function (C, A, L)")).toBe(true);
expect(embedCode.includes(`Cal ${embedType} embed code begins`)).toBe(true);
expect(embedCode).toContain("function (C, A, L)");
expect(embedCode).toContain(`Cal ${embedType} embed code begins`);
if (orgSlug) {
expect(embedCode).toContain(orgSlug);
}
const dom = parse(embedCode);
const scripts = dom.getElementsByTagName("script");
assertThatCodeIsValidVanillaJsCode(scripts[0].innerText);
return {
message: () => `passed`,
pass: true,
};
}
function assertThatCodeIsValidVanillaJsCode(code: string) {
const lintResult = linter.verify(code, {
env: {
browser: true,
},
parserOptions: {
ecmaVersion: 2021,
},
globals: {
Cal: "readonly",
},
rules: eslintRules,
});
if (lintResult.length) {
console.log(
JSON.stringify({
lintResult,
code,
})
);
}
expect(lintResult.length).toBe(0);
}
function assertThatCodeIsValidReactCode(code: string) {
const lintResult = linter.verify(code, {
env: {
browser: true,
},
parserOptions: {
ecmaVersion: 2021,
ecmaFeatures: {
jsx: true,
},
sourceType: "module",
},
rules: eslintRules,
});
if (lintResult.length) {
console.log(
JSON.stringify({
lintResult,
code,
})
);
}
expect(lintResult.length).toBe(0);
}
async function expectValidReactEmbedSnippet(
page: Page,
{ embedType, orgSlug }: { embedType: EmbedType; orgSlug: string | null }
) {
const embedCode = await page.locator("[data-testid=embed-react]").inputValue();
expect(embedCode).toContain("export default function MyApp(");
expect(embedCode).toContain(
embedType === "floating-popup" ? "floatingButton" : embedType === "inline" ? `<Cal` : "data-cal-link"
);
if (orgSlug) {
expect(embedCode).toContain(orgSlug);
}
assertThatCodeIsValidReactCode(embedCode);
return {
message: () => `passed`,
pass: true,
@ -88,141 +504,10 @@ async function expectToContainValidCode(page: Page, { embedType }: { embedType:
*/
async function expectToContainValidPreviewIframe(
page: Page,
{ embedType, calLink }: { embedType: string; calLink: string }
{ embedType, calLink, bookerUrl }: { embedType: EmbedType; calLink: string; bookerUrl?: string }
) {
const bookerUrl = `${WEBAPP_URL}`;
bookerUrl = bookerUrl || `${WEBAPP_URL}`;
expect(await page.locator("[data-testid=embed-preview]").getAttribute("src")).toContain(
`/preview.html?embedType=${embedType}&calLink=${calLink}&embedLibUrl=${EMBED_LIB_URL}&bookerUrl=${bookerUrl}`
);
}
test.describe.configure({ mode: "parallel" });
test.afterEach(({ users }) => users.deleteAll());
test.describe("Embed Code Generator Tests", () => {
test.beforeEach(async ({ users }) => {
const pro = await users.create();
await pro.apiLogin();
});
test.describe("Event Types Page", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/event-types");
});
test("open Embed Dialog and choose Inline for First Event Type", async ({ page, users }) => {
const [pro] = users.get();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "inline");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "inline",
basePage: "/event-types",
});
await expectToContainValidCode(page, { embedType: "inline" });
await gotToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "inline",
calLink: `${pro.username}/30-min`,
});
});
test("open Embed Dialog and choose floating-popup for First Event Type", async ({ page, users }) => {
const [pro] = users.get();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "floating-popup");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "floating-popup",
basePage: "/event-types",
});
await expectToContainValidCode(page, { embedType: "floating-popup" });
await gotToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "floating-popup",
calLink: `${pro.username}/30-min`,
});
});
test("open Embed Dialog and choose element-click for First Event Type", async ({ page, users }) => {
const [pro] = users.get();
const embedUrl = await clickFirstEventTypeEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage: "/event-types",
});
chooseEmbedType(page, "element-click");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
embedType: "element-click",
basePage: "/event-types",
});
await expectToContainValidCode(page, { embedType: "element-click" });
await gotToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "element-click",
calLink: `${pro.username}/30-min`,
});
});
});
test.describe("Event Type Edit Page", () => {
test.beforeEach(async ({ page }) => {
await page.goto(`/event-types`);
await Promise.all([
page.locator('a[href*="/event-types/"]').first().click(),
page.waitForURL((url) => url.pathname.startsWith("/event-types/")),
]);
});
test("open Embed Dialog for the Event Type", async ({ page }) => {
const basePage = new URL(page.url()).pathname;
const embedUrl = await clickEmbedButton(page);
await expectToBeNavigatingToEmbedTypesDialog(page, {
embedUrl,
basePage,
});
chooseEmbedType(page, "inline");
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
embedUrl,
basePage,
embedType: "inline",
});
await expectToContainValidCode(page, {
embedType: "inline",
});
await gotToPreviewTab(page);
await expectToContainValidPreviewIframe(page, {
embedType: "inline",
calLink: decodeURIComponent(embedUrl),
});
});
});
});

View File

@ -460,7 +460,7 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
}
return membership;
},
getOrg: async () => {
getOrgMembership: async () => {
return prisma.membership.findFirstOrThrow({
where: {
userId: user.id,
@ -471,7 +471,13 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
},
},
},
include: { team: { select: { children: true, metadata: true, name: true } } },
include: {
team: {
include: {
children: true,
},
},
},
});
},
getFirstEventAsOwner: async () =>

View File

@ -14,7 +14,7 @@ test.afterEach(async ({ users, emails, clipboard }) => {
test.describe("Organization", () => {
test("Invitation (non verified)", async ({ browser, page, users, emails, clipboard }) => {
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true });
const { team: org } = await orgOwner.getOrg();
const { team: org } = await orgOwner.getOrgMembership();
await orgOwner.apiLogin();
await page.goto("/settings/organizations/members");
await page.waitForLoadState("networkidle");
@ -92,7 +92,7 @@ test.describe("Organization", () => {
test("Invitation (verified)", async ({ browser, page, users, emails }) => {
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true });
const { team: org } = await orgOwner.getOrg();
const { team: org } = await orgOwner.getOrgMembership();
await orgOwner.apiLogin();
await page.goto("/settings/organizations/members");
await page.waitForLoadState("networkidle");

View File

@ -168,7 +168,7 @@ test.describe("Teams - NonOrg", () => {
await expect(page.locator("[data-testid=alert]")).toBeVisible();
// cleanup
const org = await owner.getOrg();
const org = await owner.getOrgMembership();
await prisma.team.delete({ where: { id: org.teamId } });
}
});

View File

@ -45,7 +45,7 @@ test.describe("Unpublished", () => {
test("Organization profile", async ({ users, page }) => {
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true });
const { team: org } = await owner.getOrg();
const { team: org } = await owner.getOrgMembership();
const { requestedSlug } = org.metadata as { requestedSlug: string };
await page.goto(`/org/${requestedSlug}`);
await page.waitForLoadState("networkidle");
@ -62,7 +62,7 @@ test.describe("Unpublished", () => {
isOrg: true,
hasSubteam: true,
});
const { team: org } = await owner.getOrg();
const { team: org } = await owner.getOrgMembership();
const { requestedSlug } = org.metadata as { requestedSlug: string };
const [{ slug: subteamSlug }] = org.children as { slug: string }[];
await page.goto(`/org/${requestedSlug}/team/${subteamSlug}`);
@ -80,7 +80,7 @@ test.describe("Unpublished", () => {
isOrg: true,
hasSubteam: true,
});
const { team: org } = await owner.getOrg();
const { team: org } = await owner.getOrgMembership();
const { requestedSlug } = org.metadata as { requestedSlug: string };
const [{ slug: subteamSlug, id: subteamId }] = org.children as { slug: string; id: number }[];
const { slug: subteamEventSlug } = await owner.getFirstTeamEvent(subteamId);
@ -95,7 +95,7 @@ test.describe("Unpublished", () => {
test("Organization user", async ({ users, page }) => {
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true });
const { team: org } = await owner.getOrg();
const { team: org } = await owner.getOrgMembership();
const { requestedSlug } = org.metadata as { requestedSlug: string };
await page.goto(`/org/${requestedSlug}/${owner.username}`);
await page.waitForLoadState("networkidle");
@ -107,7 +107,7 @@ test.describe("Unpublished", () => {
test("Organization user event-type", async ({ users, page }) => {
const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true });
const { team: org } = await owner.getOrg();
const { team: org } = await owner.getOrgMembership();
const { requestedSlug } = org.metadata as { requestedSlug: string };
const [{ slug: ownerEventType }] = owner.eventTypes;
await page.goto(`/org/${requestedSlug}/${owner.username}/${ownerEventType}`);

View File

@ -547,6 +547,45 @@ export function expectCalendarEventCreationFailureEmails({
);
}
export function expectSuccessfulRoudRobinReschedulingEmails({
emails,
newOrganizer,
prevOrganizer,
}: {
emails: Fixtures["emails"];
newOrganizer: { email: string; name: string };
prevOrganizer: { email: string; name: string };
}) {
if (newOrganizer !== prevOrganizer) {
// new organizer should recieve scheduling emails
expect(emails).toHaveEmail(
{
heading: "new_event_scheduled",
to: `${newOrganizer.email}`,
},
`${newOrganizer.email}`
);
// old organizer should recieve cancelled emails
expect(emails).toHaveEmail(
{
heading: "event_request_cancelled",
to: `${prevOrganizer.email}`,
},
`${prevOrganizer.email}`
);
} else {
// organizer should recieve rescheduled emails
expect(emails).toHaveEmail(
{
heading: "event_has_been_rescheduled",
to: `${newOrganizer.email}`,
},
`${newOrganizer.email}`
);
}
}
export function expectSuccessfulBookingRescheduledEmails({
emails,
organizer,

View File

@ -104,10 +104,9 @@ function generateFiles() {
* If a file has index.ts or index.tsx, it can be imported after removing the index.ts* part
*/
function getModulePath(path: string, moduleName: string) {
return (
`./${path.replace(/\\/g, "/")}/` +
moduleName.replace(/\/index\.ts|\/index\.tsx/, "").replace(/\.tsx$|\.ts$/, "")
);
return `./${path.replace(/\\/g, "/")}/${moduleName
.replace(/\/index\.ts|\/index\.tsx/, "")
.replace(/\.tsx$|\.ts$/, "")}`;
}
type ImportConfig =

View File

@ -323,7 +323,8 @@ export default class EventManager {
public async reschedule(
event: CalendarEvent,
rescheduleUid: string,
newBookingId?: number
newBookingId?: number,
changedOrganizer?: boolean
): Promise<CreateUpdateResult> {
const originalEvt = processLocation(event);
const evt = cloneDeep(originalEvt);
@ -376,32 +377,37 @@ export default class EventManager {
// As the reschedule requires confirmation, we can't update the events and meetings to new time yet. So, just delete them and let it be handled when organizer confirms the booking.
await this.deleteEventsAndMeetings({ booking, event });
} else {
// If the reschedule doesn't require confirmation, we can "update" the events and meetings to new time.
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
// If and only if event type is a dedicated meeting, update the dedicated video meeting.
if (isDedicated) {
const result = await this.updateVideoEvent(evt, booking);
const [updatedEvent] = Array.isArray(result.updatedEvent)
? result.updatedEvent
: [result.updatedEvent];
if (changedOrganizer) {
log.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking");
await this.deleteEventsAndMeetings({ booking, event });
// New event is created in handleNewBooking
} else {
// If the reschedule doesn't require confirmation, we can "update" the events and meetings to new time.
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
// If and only if event type is a dedicated meeting, update the dedicated video meeting.
if (isDedicated) {
const result = await this.updateVideoEvent(evt, booking);
const [updatedEvent] = Array.isArray(result.updatedEvent)
? result.updatedEvent
: [result.updatedEvent];
if (updatedEvent) {
evt.videoCallData = updatedEvent;
evt.location = updatedEvent.url;
if (updatedEvent) {
evt.videoCallData = updatedEvent;
evt.location = updatedEvent.url;
}
results.push(result);
}
results.push(result);
}
const bookingCalendarReference = booking.references.find((reference) =>
reference.type.includes("_calendar")
);
// There was a case that booking didn't had any reference and we don't want to throw error on function
if (bookingCalendarReference) {
// Update all calendar events.
results.push(...(await this.updateAllCalendarEvents(evt, booking, newBookingId)));
const bookingCalendarReference = booking.references.find((reference) =>
reference.type.includes("_calendar")
);
// There was a case that booking didn't had any reference and we don't want to throw error on function
if (bookingCalendarReference) {
// Update all calendar events.
results.push(...(await this.updateAllCalendarEvents(evt, booking, newBookingId)));
}
}
}
const bookingPayment = booking?.payment;
// Updating all payment to new

View File

@ -102,6 +102,37 @@ export const sendScheduledEmails = async (
await Promise.all(emailsToSend);
};
// for rescheduled round robin booking that assigned new members
export const sendRoundRobinScheduledEmails = async (calEvent: CalendarEvent, members: Person[]) => {
const emailsToSend: Promise<unknown>[] = [];
for (const teamMember of members) {
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent, teamMember })));
}
await Promise.all(emailsToSend);
};
export const sendRoundRobinRescheduledEmails = async (calEvent: CalendarEvent, members: Person[]) => {
const emailsToSend: Promise<unknown>[] = [];
for (const teamMember of members) {
emailsToSend.push(sendEmail(() => new OrganizerRescheduledEmail({ calEvent, teamMember })));
}
await Promise.all(emailsToSend);
};
export const sendRoundRobinCancelledEmails = async (calEvent: CalendarEvent, members: Person[]) => {
const emailsToSend: Promise<unknown>[] = [];
for (const teamMember of members) {
emailsToSend.push(sendEmail(() => new OrganizerCancelledEmail({ calEvent, teamMember })));
}
await Promise.all(emailsToSend);
};
export const sendRescheduledEmails = async (calEvent: CalendarEvent) => {
const emailsToSend: Promise<unknown>[] = [];

View File

@ -30,6 +30,9 @@ import {
sendOrganizerRequestEmail,
sendRescheduledEmails,
sendRescheduledSeatEmail,
sendRoundRobinCancelledEmails,
sendRoundRobinRescheduledEmails,
sendRoundRobinScheduledEmails,
sendScheduledEmails,
sendScheduledSeatsEmails,
} from "@calcom/emails";
@ -467,8 +470,26 @@ async function getOriginalRescheduledBooking(uid: string, seatsEventType?: boole
email: true,
locale: true,
timeZone: true,
destinationCalendar: true,
credentials: {
select: {
id: true,
userId: true,
key: true,
type: true,
teamId: true,
appId: true,
invalid: true,
user: {
select: {
email: true,
},
},
},
},
},
},
destinationCalendar: true,
payment: true,
references: true,
workflowReminders: true,
@ -663,8 +684,6 @@ async function handler(
};
const {
recurringCount,
allRecurringDates,
currentRecurringIndex,
noEmail,
eventTypeId,
eventTypeSlug,
@ -900,6 +919,7 @@ async function handler(
let originalRescheduledBooking: BookingType = null;
//this gets the orginal rescheduled booking
if (rescheduleUid) {
// rescheduleUid can be bookingUid and bookingSeatUid
bookingSeat = await prisma.bookingSeat.findUnique({
@ -923,6 +943,7 @@ async function handler(
}
}
//checks what users are available
if (!eventType.seatsPerTimeSlot && !skipAvailabilityCheck) {
const availableUsers = await ensureAvailableUsers(
{
@ -1897,11 +1918,16 @@ async function handler(
evt.recurringEvent = eventType.recurringEvent;
}
const changedOrganizer =
!!originalRescheduledBooking &&
eventType.schedulingType === SchedulingType.ROUND_ROBIN &&
originalRescheduledBooking.userId !== evt.organizer.id;
async function createBooking() {
if (originalRescheduledBooking) {
evt.title = originalRescheduledBooking?.title || evt.title;
evt.description = originalRescheduledBooking?.description || evt.description;
evt.location = originalRescheduledBooking?.location || evt.location;
evt.location = changedOrganizer ? evt.location : originalRescheduledBooking?.location || evt.location;
}
const eventTypeRel = !eventTypeId
@ -2146,6 +2172,7 @@ async function handler(
let videoCallUrl;
//this is the actual rescheduling logic
if (originalRescheduledBooking?.uid) {
log.silly("Rescheduling booking", originalRescheduledBooking.uid);
try {
@ -2158,9 +2185,7 @@ async function handler(
);
}
// Use EventManager to conditionally use all needed integrations.
addVideoCallDataToEvt(originalRescheduledBooking.references);
const updateManager = await eventManager.reschedule(evt, originalRescheduledBooking.uid);
//update original rescheduled booking (no seats event)
if (!eventType.seatsPerTimeSlot) {
@ -2175,6 +2200,21 @@ async function handler(
});
}
const newDesinationCalendar = evt.destinationCalendar;
evt.destinationCalendar = originalRescheduledBooking?.destinationCalendar
? [originalRescheduledBooking?.destinationCalendar]
: originalRescheduledBooking?.user?.destinationCalendar
? [originalRescheduledBooking?.user.destinationCalendar]
: evt.destinationCalendar;
const updateManager = await eventManager.reschedule(
evt,
originalRescheduledBooking.uid,
undefined,
changedOrganizer
);
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
// to the default description when we are sending the emails.
evt.description = eventType.description;
@ -2200,23 +2240,179 @@ async function handler(
: calendarResult?.updatedEvent?.iCalUID || undefined;
}
const { metadata, videoCallUrl: _videoCallUrl } = getVideoCallDetails({
const { metadata: videoMetadata, videoCallUrl: _videoCallUrl } = getVideoCallDetails({
results,
});
let metadata: AdditionalInformation = {};
metadata = videoMetadata;
videoCallUrl = _videoCallUrl;
//if organizer changed we need to create a new booking (reschedule only cancels the old one)
if (changedOrganizer) {
evt.destinationCalendar = newDesinationCalendar;
evt.title = getEventName(eventNameObject);
// location might changed and will be new created in eventManager.create (organizer default location)
evt.videoCallData = undefined;
const createManager = await eventManager.create(evt);
// This gets overridden when creating the event - to check if notes have been hidden or not. We just reset this back
// to the default description when we are sending the emails.
evt.description = eventType.description;
results = createManager.results;
referencesToCreate = createManager.referencesToCreate;
videoCallUrl = evt.videoCallData && evt.videoCallData.url ? evt.videoCallData.url : null;
if (results.length > 0 && results.every((res) => !res.success)) {
const error = {
errorCode: "BookingCreatingMeetingFailed",
message: "Booking rescheduling failed",
};
loggerWithEventDetails.error(
`EventManager.create failure in some of the integrations ${organizerUser.username}`,
safeStringify({ error, results })
);
} else {
if (results.length) {
// Handle Google Meet results
// We use the original booking location since the evt location changes to daily
if (bookingLocation === MeetLocationType) {
const googleMeetResult = {
appName: GoogleMeetMetadata.name,
type: "conferencing",
uid: results[0].uid,
originalEvent: results[0].originalEvent,
};
// Find index of google_calendar inside createManager.referencesToCreate
const googleCalIndex = createManager.referencesToCreate.findIndex(
(ref) => ref.type === "google_calendar"
);
const googleCalResult = results[googleCalIndex];
if (!googleCalResult) {
loggerWithEventDetails.warn("Google Calendar not installed but using Google Meet as location");
results.push({
...googleMeetResult,
success: false,
calWarnings: [tOrganizer("google_meet_warning")],
});
}
if (googleCalResult?.createdEvent?.hangoutLink) {
results.push({
...googleMeetResult,
success: true,
});
// Add google_meet to referencesToCreate in the same index as google_calendar
createManager.referencesToCreate[googleCalIndex] = {
...createManager.referencesToCreate[googleCalIndex],
meetingUrl: googleCalResult.createdEvent.hangoutLink,
};
// Also create a new referenceToCreate with type video for google_meet
createManager.referencesToCreate.push({
type: "google_meet_video",
meetingUrl: googleCalResult.createdEvent.hangoutLink,
uid: googleCalResult.uid,
credentialId: createManager.referencesToCreate[googleCalIndex].credentialId,
});
} else if (googleCalResult && !googleCalResult.createdEvent?.hangoutLink) {
results.push({
...googleMeetResult,
success: false,
});
}
}
metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
metadata.conferenceData = results[0].createdEvent?.conferenceData;
metadata.entryPoints = results[0].createdEvent?.entryPoints;
evt.appsStatus = handleAppsStatus(results, booking);
videoCallUrl =
metadata.hangoutLink ||
results[0].createdEvent?.url ||
organizerOrFirstDynamicGroupMemberDefaultLocationUrl ||
videoCallUrl;
}
}
}
evt.appsStatus = handleAppsStatus(results, booking);
// If there is an integration error, we don't send successful rescheduling email, instead broken integration email should be sent that are handled by either CalendarManager or videoClient
if (noEmail !== true && isConfirmedByDefault && !isThereAnIntegrationError) {
const copyEvent = cloneDeep(evt);
loggerWithEventDetails.debug("Emails: Sending rescheduled emails for booking confirmation");
await sendRescheduledEmails({
const copyEventAdditionalInfo = {
...copyEvent,
additionalInformation: metadata,
additionalNotes, // Resets back to the additionalNote input and not the override value
cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email
});
};
loggerWithEventDetails.debug("Emails: Sending rescheduled emails for booking confirmation");
/*
handle emails for round robin
- if booked rr host is the same, then rescheduling email
- if new rr host is booked, then cancellation email to old host and confirmation email to new host
*/
if (eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
const originalBookingMemberEmails: Person[] = [];
for (const user of originalRescheduledBooking.attendees) {
const translate = await getTranslation(user.locale ?? "en", "common");
originalBookingMemberEmails.push({
name: user.name,
email: user.email,
timeZone: user.timeZone,
language: { translate, locale: user.locale ?? "en" },
});
}
if (originalRescheduledBooking.user) {
const translate = await getTranslation(originalRescheduledBooking.user.locale ?? "en", "common");
originalBookingMemberEmails.push({
...originalRescheduledBooking.user,
name: originalRescheduledBooking.user.name || "",
language: { translate, locale: originalRescheduledBooking.user.locale ?? "en" },
});
}
const newBookingMemberEmails: Person[] =
copyEvent.team?.members
.map((member) => member)
.concat(copyEvent.organizer)
.concat(copyEvent.attendees) || [];
// scheduled Emails
const newBookedMembers = newBookingMemberEmails.filter(
(member) =>
!originalBookingMemberEmails.find((originalMember) => originalMember.email === member.email)
);
// cancelled Emails
const cancelledMembers = originalBookingMemberEmails.filter(
(member) => !newBookingMemberEmails.find((newMember) => newMember.email === member.email)
);
// rescheduled Emails
const rescheduledMembers = newBookingMemberEmails.filter((member) =>
originalBookingMemberEmails.find((orignalMember) => orignalMember.email === member.email)
);
sendRoundRobinRescheduledEmails(copyEventAdditionalInfo, rescheduledMembers);
sendRoundRobinScheduledEmails(copyEventAdditionalInfo, newBookedMembers);
sendRoundRobinCancelledEmails(copyEventAdditionalInfo, cancelledMembers);
} else {
// send normal rescheduled emails (non round robin event, where organizers stay the same)
await sendRescheduledEmails({
...copyEvent,
additionalInformation: metadata,
additionalNotes, // Resets back to the additionalNote input and not the override value
cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email
});
}
}
// If it's not a reschedule, doesn't require confirmation and there's no price,
// Create a booking
@ -2376,7 +2572,7 @@ async function handler(
const metadata = videoCallUrl
? {
videoCallUrl: getVideoCallUrlFromCalEvent(evt),
videoCallUrl: getVideoCallUrlFromCalEvent(evt) || videoCallUrl,
}
: undefined;

View File

@ -1,10 +1,11 @@
// import { debounce } from "lodash";
import { useSession } from "next-auth/react";
import { useSearchParams, useRouter } from "next/navigation";
import { useRouter } from "next/navigation";
import { useState, useEffect } from "react";
import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
@ -57,8 +58,8 @@ function MembersList(props: MembersListProps) {
const MembersView = () => {
const { t, i18n } = useLocale();
const router = useRouter();
const searchParams = useSearchParams();
const teamId = Number(searchParams?.get("id"));
const params = useParamsWithFallback();
const teamId = Number(params.id);
const session = useSession();
const utils = trpc.useContext();
const [offset, setOffset] = useState<number>(1);

View File

@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import type { Prisma } from "@prisma/client";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
import { useState, useLayoutEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
@ -10,6 +10,7 @@ import { z } from "zod";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { md } from "@calcom/lib/markdownIt";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import objectKeys from "@calcom/lib/objectKeys";
@ -76,8 +77,8 @@ const OtherTeamProfileView = () => {
const form = useForm({
resolver: zodResolver(teamProfileFormSchema),
});
const searchParams = useSearchParams();
const teamId = Number(searchParams?.get("id"));
const params = useParamsWithFallback();
const teamId = Number(params.id);
const { data: team, isLoading } = trpc.viewer.organizations.getOtherTeam.useQuery(
{ teamId: teamId },
{

View File

@ -1,9 +1,15 @@
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import { CAL_URL, IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import type { PreviewState } from "../types";
import { embedLibUrl } from "./constants";
import { getDimension } from "./getDimension";
export const doWeNeedCalOriginProp = (embedCalOrigin: string) => {
// If we are self hosted, calOrigin won't be app.cal.com so we need to pass it
// If we are not self hosted but it's still different from WEBAPP_URL and CAL_URL, we need to pass it -> It happens for organization booking URL at the moment
return IS_SELF_HOSTED || (embedCalOrigin !== WEBAPP_URL && embedCalOrigin !== CAL_URL);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Codes = {
react: {
@ -33,13 +39,9 @@ export const Codes = {
return <Cal
calLink="${calLink}"
style={{width:"${width}",height:"${height}",overflow:"scroll"}}
${previewState.layout ? `config={{layout: '${previewState.layout}'}}` : ""}${
IS_SELF_HOSTED
? `
calOrigin="${embedCalOrigin}"
calJsUrl="${embedLibUrl}"`
: ""
}
${previewState.layout ? `config={{layout: '${previewState.layout}'}}` : ""}
${doWeNeedCalOriginProp(embedCalOrigin) ? ` calOrigin="${embedCalOrigin}"` : ""}
${IS_SELF_HOSTED ? `calJsUrl="${embedLibUrl}"` : ""}
/>;
};`;
},
@ -53,7 +55,7 @@ export const Codes = {
return code`
import { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function App() {
export default function MyApp() {
useEffect(()=>{
(async function () {
const cal = await getCalApi(${IS_SELF_HOSTED ? `"${embedLibUrl}"` : ""});
@ -77,7 +79,7 @@ export const Codes = {
return code`
import { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function App() {
export default function MyApp() {
useEffect(()=>{
(async function () {
const cal = await getCalApi(${IS_SELF_HOSTED ? `"${embedLibUrl}"` : ""});
@ -85,7 +87,8 @@ export const Codes = {
})();
}, [])
return <button
data-cal-link="${calLink}"${IS_SELF_HOSTED ? `\ndata-cal-origin="${embedCalOrigin}"` : ""}
data-cal-link="${calLink}"
${doWeNeedCalOriginProp(embedCalOrigin) ? ` data-cal-origin="${embedCalOrigin}"` : ""}
${`data-cal-config='${JSON.stringify({
layout: previewState.layout,
})}'`}

View File

@ -2,14 +2,14 @@ import { forwardRef } from "react";
import type { MutableRefObject } from "react";
import type { BookerLayout } from "@calcom/features/bookings/Booker/types";
import { APP_NAME, IS_SELF_HOSTED } from "@calcom/lib/constants";
import { APP_NAME } from "@calcom/lib/constants";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { TextArea } from "@calcom/ui";
import { Code, Trello } from "@calcom/ui/components/icon";
import type { EmbedType, PreviewState, EmbedFramework } from "../types";
import { Codes } from "./EmbedCodes";
import { Codes, doWeNeedCalOriginProp } from "./EmbedCodes";
import { EMBED_PREVIEW_HTML_URL, embedLibUrl } from "./constants";
import { getDimension } from "./getDimension";
import { useEmbedCalOrigin } from "./hooks";
@ -193,7 +193,7 @@ const getEmbedTypeSpecificString = ({
} else if (embedType === "floating-popup") {
const floatingButtonArg = {
calLink,
...(IS_SELF_HOSTED ? { calOrigin: embedCalOrigin } : null),
...(doWeNeedCalOriginProp(embedCalOrigin) ? { calOrigin: embedCalOrigin } : null),
...previewState.floatingPopup,
};
return frameworkCodes[embedType]({