Merge branch 'main' into 12544-cal-2758-left-align-profile-view
This commit is contained in:
commit
acab4fd945
|
@ -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),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 () =>
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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 } });
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>[] = [];
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
||||
|
|
|
@ -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 },
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
})}'`}
|
||||
|
|
|
@ -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]({
|
||||
|
|
Loading…
Reference in New Issue
Block a user