From ca78be011c14489f2c268c3d3d56c7a6b52041e9 Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Fri, 1 Dec 2023 20:07:26 +0000 Subject: [PATCH] chore: [app-router-migration-1] migrate the pages in `settings/admin` to the app directory (#12561) Co-authored-by: Dmytro Hryshyn Co-authored-by: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com> Co-authored-by: zomars --- .env.example | 1 + .github/actions/cache-build/action.yml | 4 +- apps/web/abTest/middlewareFactory.ts | 2 +- apps/web/app/_trpc/serverClient.ts | 4 + apps/web/app/_trpc/trpc-provider.tsx | 27 +- apps/web/app/_types.ts | 3 + .../(admin-layout)/layout.tsx | 20 + .../settings/admin/apps/[category]/page.tsx | 10 + .../settings/admin/apps/page.tsx | 10 + .../settings/admin/flags/page.tsx | 10 + .../settings/admin/impersonation/page.tsx | 10 + .../settings/admin/oAuth/page.tsx | 10 + .../(admin-layout)/settings/admin/page.tsx | 10 + .../(no-layout)/layout.tsx | 20 + .../settings/admin/oAuth/oAuthView/page.tsx | 10 + .../(settings-layout)/layout.tsx | 21 + .../settings/admin/organizations/page.tsx | 10 + .../settings/admin/users/[id]/edit/page.tsx | 36 + .../settings/admin/users/add/page.tsx | 10 + .../settings/admin/users/page.tsx | 10 + .../auth/layouts/AdminLayoutAppDir.tsx | 40 + apps/web/middleware.ts | 2 + apps/web/next.config.js | 3 + apps/web/pages/api/email.ts | 2 +- .../pages/settings/admin/apps/[category].tsx | 2 + apps/web/pages/settings/admin/apps/index.tsx | 1 + apps/web/pages/settings/admin/flags.tsx | 2 + .../pages/settings/admin/impersonation.tsx | 2 + apps/web/pages/settings/admin/index.tsx | 2 + apps/web/pages/settings/admin/oAuth/index.tsx | 2 + .../pages/settings/admin/oAuth/oAuthView.tsx | 2 + .../settings/admin/organizations/index.tsx | 2 + .../pages/settings/admin/users/[id]/edit.tsx | 2 + apps/web/pages/settings/admin/users/add.tsx | 2 + apps/web/pages/settings/admin/users/index.tsx | 2 + apps/web/playwright/settings-admin.e2e.ts | 33 + .../web/test/utils/bookingScenario/expects.ts | 58 +- package.json | 2 +- .../emails/templates/response-email.ts | 4 +- .../routing-forms/trpc/forms.schema.ts | 2 + packages/emails/src/renderEmail.ts | 5 +- packages/emails/templates/_base-email.ts | 9 +- .../emails/templates/account-verify-email.ts | 4 +- .../admin-organization-notification.ts | 4 +- .../attendee-awaiting-payment-email.ts | 4 +- .../templates/attendee-cancelled-email.ts | 4 +- .../attendee-cancelled-seat-email.ts | 4 +- ...ee-daily-video-download-recording-email.ts | 4 +- .../templates/attendee-declined-email.ts | 4 +- .../attendee-location-change-email.ts | 4 +- .../templates/attendee-request-email.ts | 4 +- .../templates/attendee-rescheduled-email.ts | 4 +- .../templates/attendee-scheduled-email.ts | 4 +- .../emails/templates/attendee-verify-email.ts | 4 +- ...endee-was-requested-to-reschedule-email.ts | 4 +- .../templates/broken-integration-email.ts | 4 +- .../emails/templates/disabled-app-email.ts | 6 +- packages/emails/templates/feedback-email.ts | 4 +- .../emails/templates/forgot-password-email.ts | 4 +- .../emails/templates/monthly-digest-email.ts | 4 +- .../templates/no-show-fee-charged-email.ts | 4 +- .../emails/templates/org-auto-join-invite.ts | 4 +- .../organization-email-verification.ts | 4 +- ...organizer-attendee-cancelled-seat-email.ts | 4 +- .../templates/organizer-cancelled-email.ts | 4 +- ...er-daily-video-download-recording-email.ts | 4 +- .../organizer-location-change-email.ts | 4 +- .../organizer-payment-refund-failed-email.ts | 4 +- .../templates/organizer-request-email.ts | 4 +- .../organizer-request-reminder-email.ts | 4 +- ...organizer-requested-to-reschedule-email.ts | 4 +- .../templates/organizer-rescheduled-email.ts | 4 +- .../templates/organizer-scheduled-email.ts | 4 +- .../templates/slug-replacement-email.ts | 4 +- .../emails/templates/team-invite-email.ts | 4 +- .../handleNewBooking/test/reschedule.test.ts | 6 +- .../filters/lib/getTeamsFiltersFromQuery.ts | 14 +- .../settings/layouts/SettingsLayoutAppDir.tsx | 723 ++++++++++++++++++ packages/lib/hooks/useTypedQuery.ts | 2 + packages/trpc/react/shared.ts | 28 + packages/trpc/react/trpc.ts | 29 +- .../viewer/workflows/filteredList.schema.tsx | 2 + turbo.json | 1 + 83 files changed, 1193 insertions(+), 165 deletions(-) create mode 100644 apps/web/app/_trpc/serverClient.ts create mode 100644 apps/web/app/_types.ts create mode 100644 apps/web/app/future/(shared-page-wrapper)/(admin-layout)/layout.tsx create mode 100644 apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/apps/[category]/page.tsx create mode 100644 apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/apps/page.tsx create mode 100644 apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/flags/page.tsx create mode 100644 apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/impersonation/page.tsx create mode 100644 apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/oAuth/page.tsx create mode 100644 apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/page.tsx create mode 100644 apps/web/app/future/(shared-page-wrapper)/(no-layout)/layout.tsx create mode 100644 apps/web/app/future/(shared-page-wrapper)/(no-layout)/settings/admin/oAuth/oAuthView/page.tsx create mode 100644 apps/web/app/future/(shared-page-wrapper)/(settings-layout)/layout.tsx create mode 100644 apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/organizations/page.tsx create mode 100644 apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/users/[id]/edit/page.tsx create mode 100644 apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/users/add/page.tsx create mode 100644 apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/users/page.tsx create mode 100644 apps/web/components/auth/layouts/AdminLayoutAppDir.tsx create mode 100644 apps/web/playwright/settings-admin.e2e.ts create mode 100644 packages/features/settings/layouts/SettingsLayoutAppDir.tsx diff --git a/.env.example b/.env.example index c99d2dff3c..c82423c34e 100644 --- a/.env.example +++ b/.env.example @@ -290,3 +290,4 @@ E2E_TEST_OIDC_USER_PASSWORD= AB_TEST_BUCKET_PROBABILITY=50 # whether we redirect to the future/event-types from event-types or not APP_ROUTER_EVENT_TYPES_ENABLED=1 +APP_ROUTER_SETTINGS_ADMIN_ENABLED=1 \ No newline at end of file diff --git a/.github/actions/cache-build/action.yml b/.github/actions/cache-build/action.yml index 1ef35a7831..62bf25dd59 100644 --- a/.github/actions/cache-build/action.yml +++ b/.github/actions/cache-build/action.yml @@ -24,6 +24,8 @@ runs: **/.turbo/** **/dist/** key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }} - - run: yarn build + - run: | + export NODE_OPTIONS="--max_old_space_size=8192" + yarn build if: steps.cache-build.outputs.cache-hit != 'true' shell: bash diff --git a/apps/web/abTest/middlewareFactory.ts b/apps/web/abTest/middlewareFactory.ts index 5e300cf0d6..704e5decfb 100644 --- a/apps/web/abTest/middlewareFactory.ts +++ b/apps/web/abTest/middlewareFactory.ts @@ -5,6 +5,7 @@ import z from "zod"; const ROUTES: [URLPattern, boolean][] = [ ["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const, + ["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const, ].map(([pathname, enabled]) => [ new URLPattern({ pathname, @@ -27,7 +28,6 @@ export const abTestMiddlewareFactory = const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME); const route = ROUTES.find(([regExp]) => regExp.test(req.url)) ?? null; - const enabled = route !== null ? route[1] || override : false; if (pathname.includes("future") || !enabled) { diff --git a/apps/web/app/_trpc/serverClient.ts b/apps/web/app/_trpc/serverClient.ts new file mode 100644 index 0000000000..e4b7d577f0 --- /dev/null +++ b/apps/web/app/_trpc/serverClient.ts @@ -0,0 +1,4 @@ +import type { TRPCContext } from "@calcom/trpc/server/createContext"; +import { appRouter } from "@calcom/trpc/server/routers/_app"; + +export const getServerCaller = (ctx: TRPCContext) => appRouter.createCaller(ctx); diff --git a/apps/web/app/_trpc/trpc-provider.tsx b/apps/web/app/_trpc/trpc-provider.tsx index f6d2ed2817..6e81d2996a 100644 --- a/apps/web/app/_trpc/trpc-provider.tsx +++ b/apps/web/app/_trpc/trpc-provider.tsx @@ -8,33 +8,8 @@ import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink"; import { httpLink } from "@calcom/trpc/client/links/httpLink"; import { loggerLink } from "@calcom/trpc/client/links/loggerLink"; import { splitLink } from "@calcom/trpc/client/links/splitLink"; +import { ENDPOINTS } from "@calcom/trpc/react/shared"; -const ENDPOINTS = [ - "admin", - "apiKeys", - "appRoutingForms", - "apps", - "auth", - "availability", - "appBasecamp3", - "bookings", - "deploymentSetup", - "eventTypes", - "features", - "insights", - "payments", - "public", - "saml", - "slots", - "teams", - "organizations", - "users", - "viewer", - "webhook", - "workflows", - "appsRouter", - "googleWorkspace", -] as const; export type Endpoint = (typeof ENDPOINTS)[number]; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/apps/web/app/_types.ts b/apps/web/app/_types.ts new file mode 100644 index 0000000000..91f01306f4 --- /dev/null +++ b/apps/web/app/_types.ts @@ -0,0 +1,3 @@ +export type Params = { + [param: string]: string | string[] | undefined; +}; diff --git a/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/layout.tsx b/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/layout.tsx new file mode 100644 index 0000000000..ef7d2abdf2 --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/layout.tsx @@ -0,0 +1,20 @@ +import { headers } from "next/headers"; +import { type ReactElement } from "react"; + +import PageWrapper from "@components/PageWrapperAppDir"; +import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir"; + +type WrapperWithLayoutProps = { + children: ReactElement; +}; + +export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) { + const h = headers(); + const nonce = h.get("x-nonce") ?? undefined; + + return ( + + {children} + + ); +} diff --git a/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/apps/[category]/page.tsx b/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/apps/[category]/page.tsx new file mode 100644 index 0000000000..0805169a9e --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/apps/[category]/page.tsx @@ -0,0 +1,10 @@ +import Page from "@pages/settings/admin/apps/[category]"; +import { _generateMetadata } from "app/_utils"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("apps"), + (t) => t("admin_apps_description") + ); + +export default Page; diff --git a/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/apps/page.tsx b/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/apps/page.tsx new file mode 100644 index 0000000000..c53f6eb9f9 --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/apps/page.tsx @@ -0,0 +1,10 @@ +import Page from "@pages/settings/admin/apps/index"; +import { _generateMetadata } from "app/_utils"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("apps"), + (t) => t("admin_apps_description") + ); + +export default Page; diff --git a/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/flags/page.tsx b/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/flags/page.tsx new file mode 100644 index 0000000000..adf10ecbc3 --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/flags/page.tsx @@ -0,0 +1,10 @@ +import Page from "@pages/settings/admin/flags"; +import { _generateMetadata } from "app/_utils"; + +export const generateMetadata = async () => + await _generateMetadata( + () => "Feature Flags", + () => "Here you can toggle your Cal.com instance features." + ); + +export default Page; diff --git a/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/impersonation/page.tsx b/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/impersonation/page.tsx new file mode 100644 index 0000000000..f18a6df858 --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/impersonation/page.tsx @@ -0,0 +1,10 @@ +import Page from "@pages/settings/admin/impersonation"; +import { _generateMetadata } from "app/_utils"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("admin"), + (t) => t("impersonation") + ); + +export default Page; diff --git a/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/oAuth/page.tsx b/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/oAuth/page.tsx new file mode 100644 index 0000000000..61cb362dba --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/oAuth/page.tsx @@ -0,0 +1,10 @@ +import Page from "@pages/settings/admin/oAuth/index"; +import { _generateMetadata } from "app/_utils"; + +export const generateMetadata = async () => + await _generateMetadata( + () => "OAuth", + () => "Add new OAuth Clients" + ); + +export default Page; diff --git a/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/page.tsx b/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/page.tsx new file mode 100644 index 0000000000..a45fe0a58d --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(admin-layout)/settings/admin/page.tsx @@ -0,0 +1,10 @@ +import Page from "@pages/settings/admin/index"; +import { _generateMetadata } from "app/_utils"; + +export const generateMetadata = async () => + await _generateMetadata( + () => "Admin", + () => "admin_description" + ); + +export default Page; diff --git a/apps/web/app/future/(shared-page-wrapper)/(no-layout)/layout.tsx b/apps/web/app/future/(shared-page-wrapper)/(no-layout)/layout.tsx new file mode 100644 index 0000000000..c079ba0ad8 --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(no-layout)/layout.tsx @@ -0,0 +1,20 @@ +// pages containing layout (e.g., /availability/[schedule].tsx) are supposed to go under (no-layout) folder +import { headers } from "next/headers"; +import { type ReactElement } from "react"; + +import PageWrapper from "@components/PageWrapperAppDir"; + +type WrapperWithoutLayoutProps = { + children: ReactElement; +}; + +export default async function WrapperWithoutLayout({ children }: WrapperWithoutLayoutProps) { + const h = headers(); + const nonce = h.get("x-nonce") ?? undefined; + + return ( + + {children} + + ); +} diff --git a/apps/web/app/future/(shared-page-wrapper)/(no-layout)/settings/admin/oAuth/oAuthView/page.tsx b/apps/web/app/future/(shared-page-wrapper)/(no-layout)/settings/admin/oAuth/oAuthView/page.tsx new file mode 100644 index 0000000000..165622e62a --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(no-layout)/settings/admin/oAuth/oAuthView/page.tsx @@ -0,0 +1,10 @@ +import Page from "@pages/settings/admin/oAuth/oAuthView"; +import { _generateMetadata } from "app/_utils"; + +export const generateMetadata = async () => + await _generateMetadata( + () => "OAuth", + () => "Add new OAuth Clients" + ); + +export default Page; diff --git a/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/layout.tsx b/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/layout.tsx new file mode 100644 index 0000000000..2cb530db21 --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/layout.tsx @@ -0,0 +1,21 @@ +import { headers } from "next/headers"; +import { type ReactElement } from "react"; + +import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; + +import PageWrapper from "@components/PageWrapperAppDir"; + +type WrapperWithLayoutProps = { + children: ReactElement; +}; + +export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) { + const h = headers(); + const nonce = h.get("x-nonce") ?? undefined; + + return ( + + {children} + + ); +} diff --git a/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/organizations/page.tsx b/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/organizations/page.tsx new file mode 100644 index 0000000000..db1dd56ae3 --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/organizations/page.tsx @@ -0,0 +1,10 @@ +import Page from "@pages/settings/admin/organizations/index"; +import { _generateMetadata } from "app/_utils"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("organizations"), + (t) => t("orgs_page_description") + ); + +export default Page; diff --git a/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/users/[id]/edit/page.tsx b/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/users/[id]/edit/page.tsx new file mode 100644 index 0000000000..9f12efb953 --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/users/[id]/edit/page.tsx @@ -0,0 +1,36 @@ +import Page from "@pages/settings/admin/users/[id]/edit"; +import { getServerCaller } from "app/_trpc/serverClient"; +import { type Params } from "app/_types"; +import { _generateMetadata } from "app/_utils"; +import { cookies, headers } from "next/headers"; +import { z } from "zod"; + +import prisma from "@calcom/prisma"; + +const userIdSchema = z.object({ id: z.coerce.number() }); + +export const generateMetadata = async ({ params }: { params: Params }) => { + const input = userIdSchema.safeParse(params); + + let title = ""; + if (!input.success) { + title = "Editing user"; + } else { + const req = { + headers: headers(), + cookies: cookies(), + }; + + // @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest' + const data = await getServerCaller({ req, prisma }).viewer.users.get({ userId: input.data.id }); + const { user } = data; + title = `Editing user: ${user.username}`; + } + + return await _generateMetadata( + () => title, + () => "Here you can edit a current user." + ); +}; + +export default Page; diff --git a/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/users/add/page.tsx b/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/users/add/page.tsx new file mode 100644 index 0000000000..5687b0f2ea --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/users/add/page.tsx @@ -0,0 +1,10 @@ +import Page from "@pages/settings/admin/users/add"; +import { _generateMetadata } from "app/_utils"; + +export const generateMetadata = async () => + await _generateMetadata( + () => "Add new user", + () => "Here you can add a new user." + ); + +export default Page; diff --git a/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/users/page.tsx b/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/users/page.tsx new file mode 100644 index 0000000000..c63a931238 --- /dev/null +++ b/apps/web/app/future/(shared-page-wrapper)/(settings-layout)/settings/admin/users/page.tsx @@ -0,0 +1,10 @@ +import Page from "@pages/settings/admin/users/index"; +import { _generateMetadata } from "app/_utils"; + +export const generateMetadata = async () => + await _generateMetadata( + () => "Users", + () => "A list of all the users in your account including their name, title, email and role." + ); + +export default Page; diff --git a/apps/web/components/auth/layouts/AdminLayoutAppDir.tsx b/apps/web/components/auth/layouts/AdminLayoutAppDir.tsx new file mode 100644 index 0000000000..b3d76190c4 --- /dev/null +++ b/apps/web/components/auth/layouts/AdminLayoutAppDir.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { useSession } from "next-auth/react"; +import { usePathname, useRouter } from "next/navigation"; +import type { ComponentProps } from "react"; +import React, { useEffect } from "react"; + +import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout"; +import type Shell from "@calcom/features/shell/Shell"; +import { UserPermissionRole } from "@calcom/prisma/enums"; +import { ErrorBoundary } from "@calcom/ui"; + +export default function AdminLayout({ + children, + ...rest +}: { children: React.ReactNode } & ComponentProps) { + const pathname = usePathname(); + const session = useSession(); + const router = useRouter(); + + // Force redirect on component level + useEffect(() => { + if (session.data && session.data.user.role !== UserPermissionRole.ADMIN) { + router.replace("/settings/my-account/profile"); + } + }, [session, router]); + + const isAppsPage = pathname?.startsWith("/settings/admin/apps"); + return ( + +
+
*]:flex-1"}> + {children} +
+
+
+ ); +} + +export const getLayout = (page: React.ReactElement) => {page}; diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index d14b59cb45..6640a2b634 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -101,6 +101,8 @@ export const config = { "/apps/routing_forms/:path*", "/event-types", "/future/event-types/", + "/settings/admin/:path*", + "/future/settings/admin/:path*", ], }; diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 24d6ceb2fc..1f3935bf5f 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -231,6 +231,9 @@ const nextConfig = { ...config.resolve.fallback, // if you miss it, all the other options in fallback, specified // by next.js will be dropped. Doesn't make much sense, but how it is fs: false, + // ignore module resolve errors caused by the server component bundler + "pg-native": false, + "superagent-proxy": false, }; /** diff --git a/apps/web/pages/api/email.ts b/apps/web/pages/api/email.ts index b855eccee2..9ca6337d32 100644 --- a/apps/web/pages/api/email.ts +++ b/apps/web/pages/api/email.ts @@ -13,7 +13,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { res.setHeader("Content-Type", "text/html"); res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate"); res.write( - renderEmail("MonthlyDigestEmail", { + await renderEmail("MonthlyDigestEmail", { language: t, Created: 12, Completed: 13, diff --git a/apps/web/pages/settings/admin/apps/[category].tsx b/apps/web/pages/settings/admin/apps/[category].tsx index d16c454cd4..59945469b7 100644 --- a/apps/web/pages/settings/admin/apps/[category].tsx +++ b/apps/web/pages/settings/admin/apps/[category].tsx @@ -1,3 +1,5 @@ +"use client"; + import AdminAppsList from "@calcom/features/apps/AdminAppsList"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Meta } from "@calcom/ui"; diff --git a/apps/web/pages/settings/admin/apps/index.tsx b/apps/web/pages/settings/admin/apps/index.tsx index 999ebcf164..26606937d8 100644 --- a/apps/web/pages/settings/admin/apps/index.tsx +++ b/apps/web/pages/settings/admin/apps/index.tsx @@ -1 +1,2 @@ +"use client"; export { default } from "./[category]"; diff --git a/apps/web/pages/settings/admin/flags.tsx b/apps/web/pages/settings/admin/flags.tsx index dba5c307de..6115198461 100644 --- a/apps/web/pages/settings/admin/flags.tsx +++ b/apps/web/pages/settings/admin/flags.tsx @@ -1,3 +1,5 @@ +"use client"; + import { FlagListingView } from "@calcom/features/flags/pages/flag-listing-view"; import PageWrapper from "@components/PageWrapper"; diff --git a/apps/web/pages/settings/admin/impersonation.tsx b/apps/web/pages/settings/admin/impersonation.tsx index 5e7f02c661..6bd524e77a 100644 --- a/apps/web/pages/settings/admin/impersonation.tsx +++ b/apps/web/pages/settings/admin/impersonation.tsx @@ -1,3 +1,5 @@ +"use client"; + import { signIn } from "next-auth/react"; import { useRef } from "react"; diff --git a/apps/web/pages/settings/admin/index.tsx b/apps/web/pages/settings/admin/index.tsx index 27122c8849..2193253fda 100644 --- a/apps/web/pages/settings/admin/index.tsx +++ b/apps/web/pages/settings/admin/index.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Meta } from "@calcom/ui"; import PageWrapper from "@components/PageWrapper"; diff --git a/apps/web/pages/settings/admin/oAuth/index.tsx b/apps/web/pages/settings/admin/oAuth/index.tsx index dec29e4af5..73568da45c 100644 --- a/apps/web/pages/settings/admin/oAuth/index.tsx +++ b/apps/web/pages/settings/admin/oAuth/index.tsx @@ -1,3 +1,5 @@ +"use client"; + import PageWrapper from "@components/PageWrapper"; import { getLayout } from "@components/auth/layouts/AdminLayout"; diff --git a/apps/web/pages/settings/admin/oAuth/oAuthView.tsx b/apps/web/pages/settings/admin/oAuth/oAuthView.tsx index a6a16463cf..28110a3ec4 100644 --- a/apps/web/pages/settings/admin/oAuth/oAuthView.tsx +++ b/apps/web/pages/settings/admin/oAuth/oAuthView.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useState } from "react"; import { useForm } from "react-hook-form"; diff --git a/apps/web/pages/settings/admin/organizations/index.tsx b/apps/web/pages/settings/admin/organizations/index.tsx index 9c2d7cc1de..4937f158c2 100644 --- a/apps/web/pages/settings/admin/organizations/index.tsx +++ b/apps/web/pages/settings/admin/organizations/index.tsx @@ -1,3 +1,5 @@ +"use client"; + import AdminOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage"; import type { CalPageWrapper } from "@components/PageWrapper"; diff --git a/apps/web/pages/settings/admin/users/[id]/edit.tsx b/apps/web/pages/settings/admin/users/[id]/edit.tsx index 0ee1269cca..3969862d83 100644 --- a/apps/web/pages/settings/admin/users/[id]/edit.tsx +++ b/apps/web/pages/settings/admin/users/[id]/edit.tsx @@ -1,3 +1,5 @@ +"use client"; + import UsersEditView from "@calcom/features/ee/users/pages/users-edit-view"; import type { CalPageWrapper } from "@components/PageWrapper"; diff --git a/apps/web/pages/settings/admin/users/add.tsx b/apps/web/pages/settings/admin/users/add.tsx index 293a90214e..b4afda4e4b 100644 --- a/apps/web/pages/settings/admin/users/add.tsx +++ b/apps/web/pages/settings/admin/users/add.tsx @@ -1,3 +1,5 @@ +"use client"; + import UsersAddView from "@calcom/features/ee/users/pages/users-add-view"; import type { CalPageWrapper } from "@components/PageWrapper"; diff --git a/apps/web/pages/settings/admin/users/index.tsx b/apps/web/pages/settings/admin/users/index.tsx index 194c966682..d822ee7dab 100644 --- a/apps/web/pages/settings/admin/users/index.tsx +++ b/apps/web/pages/settings/admin/users/index.tsx @@ -1,3 +1,5 @@ +"use client"; + import UsersListingView from "@calcom/features/ee/users/pages/users-listing-view"; import type { CalPageWrapper } from "@components/PageWrapper"; diff --git a/apps/web/playwright/settings-admin.e2e.ts b/apps/web/playwright/settings-admin.e2e.ts new file mode 100644 index 0000000000..d1f15f281c --- /dev/null +++ b/apps/web/playwright/settings-admin.e2e.ts @@ -0,0 +1,33 @@ +import { expect } from "@playwright/test"; + +import { test } from "./lib/fixtures"; + +test.describe.configure({ mode: "parallel" }); + +test.describe("Settings/admin A/B tests", () => { + test("should point to the /future/settings/admin page", async ({ page, users, context }) => { + await context.addCookies([ + { + name: "x-calcom-future-routes-override", + value: "1", + url: "http://localhost:3000", + }, + ]); + const user = await users.create(); + await user.apiLogin(); + + await page.goto("/settings/admin"); + + await page.waitForLoadState(); + + const dataNextJsRouter = await page.evaluate(() => + window.document.documentElement.getAttribute("data-nextjs-router") + ); + + expect(dataNextJsRouter).toEqual("app"); + + const locator = page.getByRole("heading", { name: "Feature Flags" }); + + await expect(locator).toBeVisible(); + }); +}); diff --git a/apps/web/test/utils/bookingScenario/expects.ts b/apps/web/test/utils/bookingScenario/expects.ts index 5db6e68c4c..edb5e1a9a1 100644 --- a/apps/web/test/utils/bookingScenario/expects.ts +++ b/apps/web/test/utils/bookingScenario/expects.ts @@ -4,7 +4,7 @@ import type { WebhookTriggerEvents, Booking, BookingReference, DestinationCalend import { parse } from "node-html-parser"; import type { VEvent } from "node-ical"; import ical from "node-ical"; -import { expect } from "vitest"; +import { expect, vi } from "vitest"; import "vitest-fetch-mock"; import dayjs from "@calcom/dayjs"; @@ -547,7 +547,7 @@ export function expectCalendarEventCreationFailureEmails({ ); } -export function expectSuccessfulRoudRobinReschedulingEmails({ +export function expectSuccessfulRoundRobinReschedulingEmails({ emails, newOrganizer, prevOrganizer, @@ -557,32 +557,38 @@ export function expectSuccessfulRoudRobinReschedulingEmails({ 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}` - ); + vi.waitFor(() => { + // 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}` - ); + vi.waitFor(() => { + // 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}` - ); + vi.waitFor(() => { + // organizer should recieve rescheduled emails + expect(emails).toHaveEmail( + { + heading: "event_has_been_rescheduled", + to: `${newOrganizer.email}`, + }, + `${newOrganizer.email}` + ); + }); } } diff --git a/package.json b/package.json index 586870f28e..8cb73848c1 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "prismock": "^1.21.1", "tsc-absolute": "^1.0.0", "typescript": "^4.9.4", - "vitest": "^0.34.3", + "vitest": "^0.34.6", "vitest-fetch-mock": "^0.2.2", "vitest-mock-extended": "^1.1.3" }, diff --git a/packages/app-store/routing-forms/emails/templates/response-email.ts b/packages/app-store/routing-forms/emails/templates/response-email.ts index 0fa84b8ae6..16e421184d 100644 --- a/packages/app-store/routing-forms/emails/templates/response-email.ts +++ b/packages/app-store/routing-forms/emails/templates/response-email.ts @@ -25,14 +25,14 @@ export default class ResponseEmail extends BaseEmail { this.toAddresses = toAddresses; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { const toAddresses = this.toAddresses; const subject = `${this.form.name} has a new response`; return { from: `Cal.com <${this.getMailerOptions().from}>`, to: toAddresses.join(","), subject, - html: renderEmail("ResponseEmail", { + html: await renderEmail("ResponseEmail", { form: this.form, orderedResponses: this.orderedResponses, subject, diff --git a/packages/app-store/routing-forms/trpc/forms.schema.ts b/packages/app-store/routing-forms/trpc/forms.schema.ts index 787d6e2be7..343097874f 100644 --- a/packages/app-store/routing-forms/trpc/forms.schema.ts +++ b/packages/app-store/routing-forms/trpc/forms.schema.ts @@ -1,3 +1,5 @@ +"use client"; + import { z } from "zod"; import { filterQuerySchemaStrict } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery"; diff --git a/packages/emails/src/renderEmail.ts b/packages/emails/src/renderEmail.ts index 4404c94a47..4ada467b55 100644 --- a/packages/emails/src/renderEmail.ts +++ b/packages/emails/src/renderEmail.ts @@ -1,12 +1,11 @@ -import * as ReactDOMServer from "react-dom/server"; - import * as templates from "./templates"; -function renderEmail( +async function renderEmail( template: K, props: React.ComponentProps<(typeof templates)[K]> ) { const Component = templates[template]; + const ReactDOMServer = (await import("react-dom/server")).default; return ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error diff --git a/packages/emails/templates/_base-email.ts b/packages/emails/templates/_base-email.ts index 51159a198d..0fd65be9d7 100644 --- a/packages/emails/templates/_base-email.ts +++ b/packages/emails/templates/_base-email.ts @@ -24,7 +24,7 @@ export default class BaseEmail { return dayjs(time).tz(this.getTimezone()).locale(this.getLocale()).format(format); } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return {}; } public async sendEmail() { @@ -38,21 +38,20 @@ export default class BaseEmail { if (process.env.INTEGRATION_TEST_MODE === "true") { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-expect-error - setTestEmail(this.getNodeMailerPayload()); + setTestEmail(await this.getNodeMailerPayload()); console.log( "Skipped Sending Email as process.env.NEXT_PUBLIC_UNIT_TESTS is set. Emails are available in globalThis.testEmails" ); return new Promise((r) => r("Skipped sendEmail for Unit Tests")); } - const payload = this.getNodeMailerPayload(); + const payload = await this.getNodeMailerPayload(); const parseSubject = z.string().safeParse(payload?.subject); const payloadWithUnEscapedSubject = { headers: this.getMailerOptions().headers, ...payload, ...(parseSubject.success && { subject: decodeHTML(parseSubject.data) }), }; - await new Promise((resolve, reject) => createTransport(this.getMailerOptions().transport).sendMail( payloadWithUnEscapedSubject, @@ -69,7 +68,6 @@ export default class BaseEmail { ).catch((e) => console.error("sendEmail", e)); return new Promise((resolve) => resolve("send mail async")); } - protected getMailerOptions() { return { transport: serverConfig.transport, @@ -77,7 +75,6 @@ export default class BaseEmail { headers: serverConfig.headers, }; } - protected printNodeMailerError(error: Error): void { /** Don't clog the logs with unsent emails in E2E */ if (process.env.NEXT_PUBLIC_IS_E2E) return; diff --git a/packages/emails/templates/account-verify-email.ts b/packages/emails/templates/account-verify-email.ts index 74a651ac00..6f692898aa 100644 --- a/packages/emails/templates/account-verify-email.ts +++ b/packages/emails/templates/account-verify-email.ts @@ -23,14 +23,14 @@ export default class AccountVerifyEmail extends BaseEmail { this.verifyAccountInput = passwordEvent; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { to: `${this.verifyAccountInput.user.name} <${this.verifyAccountInput.user.email}>`, from: `${APP_NAME} <${this.getMailerOptions().from}>`, subject: this.verifyAccountInput.language("verify_email_subject", { appName: APP_NAME, }), - html: renderEmail("VerifyAccountEmail", this.verifyAccountInput), + html: await renderEmail("VerifyAccountEmail", this.verifyAccountInput), text: this.getTextBody(), }; } diff --git a/packages/emails/templates/admin-organization-notification.ts b/packages/emails/templates/admin-organization-notification.ts index 680add21d0..01b6f469ab 100644 --- a/packages/emails/templates/admin-organization-notification.ts +++ b/packages/emails/templates/admin-organization-notification.ts @@ -22,12 +22,12 @@ export default class AdminOrganizationNotification extends BaseEmail { this.input = input; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { from: `${APP_NAME} <${this.getMailerOptions().from}>`, to: this.input.instanceAdmins.map((admin) => admin.email).join(","), subject: `${this.input.t("admin_org_notification_email_subject")}`, - html: renderEmail("AdminOrganizationNotificationEmail", { + html: await renderEmail("AdminOrganizationNotificationEmail", { orgSlug: this.input.orgSlug, webappIPAddress: this.input.webappIPAddress, language: this.input.t, diff --git a/packages/emails/templates/attendee-awaiting-payment-email.ts b/packages/emails/templates/attendee-awaiting-payment-email.ts index 772b55c10b..24eef1262c 100644 --- a/packages/emails/templates/attendee-awaiting-payment-email.ts +++ b/packages/emails/templates/attendee-awaiting-payment-email.ts @@ -2,7 +2,7 @@ import { renderEmail } from "../"; import AttendeeScheduledEmail from "./attendee-scheduled-email"; export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { to: `${this.attendee.name} <${this.attendee.email}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, @@ -11,7 +11,7 @@ export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail title: this.calEvent.title, date: this.getFormattedDate(), })}`, - html: renderEmail("AttendeeAwaitingPaymentEmail", { + html: await renderEmail("AttendeeAwaitingPaymentEmail", { calEvent: this.calEvent, attendee: this.attendee, }), diff --git a/packages/emails/templates/attendee-cancelled-email.ts b/packages/emails/templates/attendee-cancelled-email.ts index ecc128cadc..e3cb71b54a 100644 --- a/packages/emails/templates/attendee-cancelled-email.ts +++ b/packages/emails/templates/attendee-cancelled-email.ts @@ -2,7 +2,7 @@ import { renderEmail } from "../"; import AttendeeScheduledEmail from "./attendee-scheduled-email"; export default class AttendeeCancelledEmail extends AttendeeScheduledEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { to: `${this.attendee.name} <${this.attendee.email}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, @@ -11,7 +11,7 @@ export default class AttendeeCancelledEmail extends AttendeeScheduledEmail { title: this.calEvent.title, date: this.getFormattedDate(), })}`, - html: renderEmail("AttendeeCancelledEmail", { + html: await renderEmail("AttendeeCancelledEmail", { calEvent: this.calEvent, attendee: this.attendee, }), diff --git a/packages/emails/templates/attendee-cancelled-seat-email.ts b/packages/emails/templates/attendee-cancelled-seat-email.ts index 396891800c..610274732e 100644 --- a/packages/emails/templates/attendee-cancelled-seat-email.ts +++ b/packages/emails/templates/attendee-cancelled-seat-email.ts @@ -2,7 +2,7 @@ import { renderEmail } from "../"; import AttendeeScheduledEmail from "./attendee-scheduled-email"; export default class AttendeeCancelledSeatEmail extends AttendeeScheduledEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { to: `${this.attendee.name} <${this.attendee.email}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, @@ -11,7 +11,7 @@ export default class AttendeeCancelledSeatEmail extends AttendeeScheduledEmail { title: this.calEvent.title, date: this.getFormattedDate(), })}`, - html: renderEmail("AttendeeCancelledSeatEmail", { + html: await renderEmail("AttendeeCancelledSeatEmail", { calEvent: this.calEvent, attendee: this.attendee, }), diff --git a/packages/emails/templates/attendee-daily-video-download-recording-email.ts b/packages/emails/templates/attendee-daily-video-download-recording-email.ts index 7fc0a74d52..76cfaff690 100644 --- a/packages/emails/templates/attendee-daily-video-download-recording-email.ts +++ b/packages/emails/templates/attendee-daily-video-download-recording-email.ts @@ -21,7 +21,7 @@ export default class AttendeeDailyVideoDownloadRecordingEmail extends BaseEmail this.downloadLink = downloadLink; this.t = attendee.language.translate; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { to: `${this.attendee.name} <${this.attendee.email}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, @@ -30,7 +30,7 @@ export default class AttendeeDailyVideoDownloadRecordingEmail extends BaseEmail title: this.calEvent.title, date: this.getFormattedDate(), })}`, - html: renderEmail("DailyVideoDownloadRecordingEmail", { + html: await renderEmail("DailyVideoDownloadRecordingEmail", { title: this.calEvent.title, date: this.getFormattedDate(), downloadLink: this.downloadLink, diff --git a/packages/emails/templates/attendee-declined-email.ts b/packages/emails/templates/attendee-declined-email.ts index 2d7fe6d33b..8ff713332c 100644 --- a/packages/emails/templates/attendee-declined-email.ts +++ b/packages/emails/templates/attendee-declined-email.ts @@ -2,7 +2,7 @@ import { renderEmail } from "../"; import AttendeeScheduledEmail from "./attendee-scheduled-email"; export default class AttendeeDeclinedEmail extends AttendeeScheduledEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { to: `${this.attendee.name} <${this.attendee.email}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, @@ -11,7 +11,7 @@ export default class AttendeeDeclinedEmail extends AttendeeScheduledEmail { title: this.calEvent.title, date: this.getFormattedDate(), })}`, - html: renderEmail("AttendeeDeclinedEmail", { + html: await renderEmail("AttendeeDeclinedEmail", { calEvent: this.calEvent, attendee: this.attendee, }), diff --git a/packages/emails/templates/attendee-location-change-email.ts b/packages/emails/templates/attendee-location-change-email.ts index 925ba0806d..af15fec058 100644 --- a/packages/emails/templates/attendee-location-change-email.ts +++ b/packages/emails/templates/attendee-location-change-email.ts @@ -2,7 +2,7 @@ import { renderEmail } from "../"; import AttendeeScheduledEmail from "./attendee-scheduled-email"; export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { icalEvent: { filename: "event.ics", @@ -16,7 +16,7 @@ export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail name: this.calEvent.team?.name || this.calEvent.organizer.name, date: this.getFormattedDate(), })}`, - html: renderEmail("AttendeeLocationChangeEmail", { + html: await renderEmail("AttendeeLocationChangeEmail", { calEvent: this.calEvent, attendee: this.attendee, }), diff --git a/packages/emails/templates/attendee-request-email.ts b/packages/emails/templates/attendee-request-email.ts index 22367aaa50..3f5d555b7c 100644 --- a/packages/emails/templates/attendee-request-email.ts +++ b/packages/emails/templates/attendee-request-email.ts @@ -4,7 +4,7 @@ import { renderEmail } from "../"; import AttendeeScheduledEmail from "./attendee-scheduled-email"; export default class AttendeeRequestEmail extends AttendeeScheduledEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { const toAddresses = this.calEvent.attendees.map((attendee) => attendee.email); return { @@ -15,7 +15,7 @@ export default class AttendeeRequestEmail extends AttendeeScheduledEmail { title: this.calEvent.title, date: this.getFormattedDate(), })}`, - html: renderEmail("AttendeeRequestEmail", { + html: await renderEmail("AttendeeRequestEmail", { calEvent: this.calEvent, attendee: this.attendee, }), diff --git a/packages/emails/templates/attendee-rescheduled-email.ts b/packages/emails/templates/attendee-rescheduled-email.ts index 0c7e183335..85bc7543ca 100644 --- a/packages/emails/templates/attendee-rescheduled-email.ts +++ b/packages/emails/templates/attendee-rescheduled-email.ts @@ -2,7 +2,7 @@ import { renderEmail } from "../"; import AttendeeScheduledEmail from "./attendee-scheduled-email"; export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { icalEvent: { filename: "event.ics", @@ -15,7 +15,7 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail { title: this.calEvent.title, date: this.getFormattedDate(), })}`, - html: renderEmail("AttendeeRescheduledEmail", { + html: await renderEmail("AttendeeRescheduledEmail", { calEvent: this.calEvent, attendee: this.attendee, }), diff --git a/packages/emails/templates/attendee-scheduled-email.ts b/packages/emails/templates/attendee-scheduled-email.ts index 0d22891118..c2dba8dcc5 100644 --- a/packages/emails/templates/attendee-scheduled-email.ts +++ b/packages/emails/templates/attendee-scheduled-email.ts @@ -82,7 +82,7 @@ export default class AttendeeScheduledEmail extends BaseEmail { return icsEvent.value; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { const clonedCalEvent = cloneDeep(this.calEvent); this.getiCalEventAsString(); @@ -97,7 +97,7 @@ export default class AttendeeScheduledEmail extends BaseEmail { from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, replyTo: [...this.calEvent.attendees.map(({ email }) => email), this.calEvent.organizer.email], subject: `${this.calEvent.title}`, - html: renderEmail("AttendeeScheduledEmail", { + html: await renderEmail("AttendeeScheduledEmail", { calEvent: clonedCalEvent, attendee: this.attendee, }), diff --git a/packages/emails/templates/attendee-verify-email.ts b/packages/emails/templates/attendee-verify-email.ts index 99e9e3e31f..7919b8aa34 100644 --- a/packages/emails/templates/attendee-verify-email.ts +++ b/packages/emails/templates/attendee-verify-email.ts @@ -23,14 +23,14 @@ export default class AttendeeVerifyEmail extends BaseEmail { this.verifyAccountInput = passwordEvent; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { to: `${this.verifyAccountInput.user.name} <${this.verifyAccountInput.user.email}>`, from: `${APP_NAME} <${this.getMailerOptions().from}>`, subject: this.verifyAccountInput.language("verify_email_subject", { appName: APP_NAME, }), - html: renderEmail("VerifyEmailByCode", this.verifyAccountInput), + html: await renderEmail("VerifyEmailByCode", this.verifyAccountInput), text: this.getTextBody(), }; } diff --git a/packages/emails/templates/attendee-was-requested-to-reschedule-email.ts b/packages/emails/templates/attendee-was-requested-to-reschedule-email.ts index 184cbf4065..e5f11807a9 100644 --- a/packages/emails/templates/attendee-was-requested-to-reschedule-email.ts +++ b/packages/emails/templates/attendee-was-requested-to-reschedule-email.ts @@ -16,7 +16,7 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche this.metadata = metadata; this.t = this.calEvent.attendees[0].language.translate; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { const toAddresses = [this.calEvent.attendees[0].email]; return { @@ -30,7 +30,7 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche eventType: this.calEvent.type, name: this.calEvent.attendees[0].name, })}`, - html: renderEmail("AttendeeWasRequestedToRescheduleEmail", { + html: await renderEmail("AttendeeWasRequestedToRescheduleEmail", { calEvent: this.calEvent, attendee: this.calEvent.attendees[0], metadata: this.metadata, diff --git a/packages/emails/templates/broken-integration-email.ts b/packages/emails/templates/broken-integration-email.ts index 12d7800a89..1cbba4c6f1 100644 --- a/packages/emails/templates/broken-integration-email.ts +++ b/packages/emails/templates/broken-integration-email.ts @@ -21,7 +21,7 @@ export default class BrokenIntegrationEmail extends BaseEmail { this.type = type; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { const toAddresses = [this.calEvent.organizer.email]; return { @@ -32,7 +32,7 @@ export default class BrokenIntegrationEmail extends BaseEmail { name: this.calEvent.attendees[0].name, date: this.getFormattedDate(), })}`, - html: renderEmail("BrokenIntegrationEmail", { + html: await renderEmail("BrokenIntegrationEmail", { calEvent: this.calEvent, attendee: this.calEvent.organizer, type: this.type, diff --git a/packages/emails/templates/disabled-app-email.ts b/packages/emails/templates/disabled-app-email.ts index 0a927a179c..724bed66e1 100644 --- a/packages/emails/templates/disabled-app-email.ts +++ b/packages/emails/templates/disabled-app-email.ts @@ -1,4 +1,4 @@ -import { TFunction } from "next-i18next"; +import type { TFunction } from "next-i18next"; import { renderEmail } from ".."; import BaseEmail from "./_base-email"; @@ -28,7 +28,7 @@ export default class DisabledAppEmail extends BaseEmail { this.eventTypeId = eventTypeId; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { from: `Cal.com <${this.getMailerOptions().from}>`, to: this.email, @@ -36,7 +36,7 @@ export default class DisabledAppEmail extends BaseEmail { this.title && this.eventTypeId ? this.t("disabled_app_affects_event_type", { appName: this.appName, eventType: this.title }) : this.t("admin_has_disabled", { appName: this.appName }), - html: renderEmail("DisabledAppEmail", { + html: await renderEmail("DisabledAppEmail", { title: this.title, appName: this.appName, eventTypeId: this.eventTypeId, diff --git a/packages/emails/templates/feedback-email.ts b/packages/emails/templates/feedback-email.ts index e0dd9e5d0f..4eef2a9f34 100644 --- a/packages/emails/templates/feedback-email.ts +++ b/packages/emails/templates/feedback-email.ts @@ -18,12 +18,12 @@ export default class FeedbackEmail extends BaseEmail { this.feedback = feedback; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { from: `${APP_NAME} <${this.getMailerOptions().from}>`, to: process.env.SEND_FEEDBACK_EMAIL, subject: `User Feedback`, - html: renderEmail("FeedbackEmail", this.feedback), + html: await renderEmail("FeedbackEmail", this.feedback), text: this.getTextBody(), }; } diff --git a/packages/emails/templates/forgot-password-email.ts b/packages/emails/templates/forgot-password-email.ts index 6c21606cc6..7f041693b7 100644 --- a/packages/emails/templates/forgot-password-email.ts +++ b/packages/emails/templates/forgot-password-email.ts @@ -23,14 +23,14 @@ export default class ForgotPasswordEmail extends BaseEmail { this.passwordEvent = passwordEvent; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { to: `${this.passwordEvent.user.name} <${this.passwordEvent.user.email}>`, from: `${APP_NAME} <${this.getMailerOptions().from}>`, subject: this.passwordEvent.language("reset_password_subject", { appName: APP_NAME, }), - html: renderEmail("ForgotPasswordEmail", this.passwordEvent), + html: await renderEmail("ForgotPasswordEmail", this.passwordEvent), text: this.getTextBody(), }; } diff --git a/packages/emails/templates/monthly-digest-email.ts b/packages/emails/templates/monthly-digest-email.ts index 5230732f3a..629b389329 100644 --- a/packages/emails/templates/monthly-digest-email.ts +++ b/packages/emails/templates/monthly-digest-email.ts @@ -12,12 +12,12 @@ export default class MonthlyDigestEmail extends BaseEmail { this.eventData = eventData; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { from: `${APP_NAME} <${this.getMailerOptions().from}>`, to: this.eventData.admin.email, subject: `${APP_NAME}: Your monthly digest`, - html: renderEmail("MonthlyDigestEmail", this.eventData), + html: await renderEmail("MonthlyDigestEmail", this.eventData), text: "", }; } diff --git a/packages/emails/templates/no-show-fee-charged-email.ts b/packages/emails/templates/no-show-fee-charged-email.ts index 500e4393eb..3ede12d719 100644 --- a/packages/emails/templates/no-show-fee-charged-email.ts +++ b/packages/emails/templates/no-show-fee-charged-email.ts @@ -2,7 +2,7 @@ import { renderEmail } from "../"; import AttendeeScheduledEmail from "./attendee-scheduled-email"; export default class NoShowFeeChargedEmail extends AttendeeScheduledEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { if (!this.calEvent.paymentInfo?.amount) throw new Error("No payment into"); return { to: `${this.attendee.name} <${this.attendee.email}>`, @@ -14,7 +14,7 @@ export default class NoShowFeeChargedEmail extends AttendeeScheduledEmail { amount: this.calEvent.paymentInfo.amount / 100, formatParams: { amount: { currency: this.calEvent.paymentInfo?.currency } }, })}`, - html: renderEmail("NoShowFeeChargedEmail", { + html: await renderEmail("NoShowFeeChargedEmail", { calEvent: this.calEvent, attendee: this.attendee, }), diff --git a/packages/emails/templates/org-auto-join-invite.ts b/packages/emails/templates/org-auto-join-invite.ts index 002ebf7482..64a2811ec6 100644 --- a/packages/emails/templates/org-auto-join-invite.ts +++ b/packages/emails/templates/org-auto-join-invite.ts @@ -22,7 +22,7 @@ export default class OrgAutoJoinEmail extends BaseEmail { this.orgAutoInviteEvent = orgAutoInviteEvent; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { to: this.orgAutoInviteEvent.to, from: `${APP_NAME} <${this.getMailerOptions().from}>`, @@ -32,7 +32,7 @@ export default class OrgAutoJoinEmail extends BaseEmail { appName: APP_NAME, entity: this.orgAutoInviteEvent.language("organization").toLowerCase(), }), - html: renderEmail("OrgAutoInviteEmail", this.orgAutoInviteEvent), + html: await renderEmail("OrgAutoInviteEmail", this.orgAutoInviteEvent), text: "", }; } diff --git a/packages/emails/templates/organization-email-verification.ts b/packages/emails/templates/organization-email-verification.ts index cfbc591df6..89cfec56f4 100644 --- a/packages/emails/templates/organization-email-verification.ts +++ b/packages/emails/templates/organization-email-verification.ts @@ -22,12 +22,12 @@ export default class OrganizationEmailVerification extends BaseEmail { this.orgVerifyInput = orgVerifyInput; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { from: `${APP_NAME} <${this.getMailerOptions().from}>`, to: this.orgVerifyInput.user.email, subject: this.orgVerifyInput.language("verify_email_organization"), - html: renderEmail("OrganisationAccountVerifyEmail", this.orgVerifyInput), + html: await renderEmail("OrganisationAccountVerifyEmail", this.orgVerifyInput), text: this.getTextBody(), }; } diff --git a/packages/emails/templates/organizer-attendee-cancelled-seat-email.ts b/packages/emails/templates/organizer-attendee-cancelled-seat-email.ts index fe6ce75726..5fa17765b1 100644 --- a/packages/emails/templates/organizer-attendee-cancelled-seat-email.ts +++ b/packages/emails/templates/organizer-attendee-cancelled-seat-email.ts @@ -4,7 +4,7 @@ import { renderEmail } from "../"; import OrganizerScheduledEmail from "./organizer-scheduled-email"; export default class OrganizerCancelledEmail extends OrganizerScheduledEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { const toAddresses = [this.calEvent.organizer.email]; if (this.calEvent.team) { this.calEvent.team.members.forEach((member) => { @@ -22,7 +22,7 @@ export default class OrganizerCancelledEmail extends OrganizerScheduledEmail { title: this.calEvent.title, date: this.getFormattedDate(), })}`, - html: renderEmail("OrganizerAttendeeCancelledSeatEmail", { + html: await renderEmail("OrganizerAttendeeCancelledSeatEmail", { attendee: this.calEvent.organizer, calEvent: this.calEvent, }), diff --git a/packages/emails/templates/organizer-cancelled-email.ts b/packages/emails/templates/organizer-cancelled-email.ts index d0e2fb315c..3c3792e2a3 100644 --- a/packages/emails/templates/organizer-cancelled-email.ts +++ b/packages/emails/templates/organizer-cancelled-email.ts @@ -4,7 +4,7 @@ import { renderEmail } from "../"; import OrganizerScheduledEmail from "./organizer-scheduled-email"; export default class OrganizerCancelledEmail extends OrganizerScheduledEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; return { @@ -14,7 +14,7 @@ export default class OrganizerCancelledEmail extends OrganizerScheduledEmail { title: this.calEvent.title, date: this.getFormattedDate(), })}`, - html: renderEmail("OrganizerCancelledEmail", { + html: await renderEmail("OrganizerCancelledEmail", { attendee: this.calEvent.organizer, calEvent: this.calEvent, }), diff --git a/packages/emails/templates/organizer-daily-video-download-recording-email.ts b/packages/emails/templates/organizer-daily-video-download-recording-email.ts index 714ba4f7e4..34e1549132 100644 --- a/packages/emails/templates/organizer-daily-video-download-recording-email.ts +++ b/packages/emails/templates/organizer-daily-video-download-recording-email.ts @@ -19,7 +19,7 @@ export default class OrganizerDailyVideoDownloadRecordingEmail extends BaseEmail this.downloadLink = downloadLink; this.t = this.calEvent.organizer.language.translate; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { to: `${this.calEvent.organizer.email}>`, from: `${APP_NAME} <${this.getMailerOptions().from}>`, @@ -28,7 +28,7 @@ export default class OrganizerDailyVideoDownloadRecordingEmail extends BaseEmail title: this.calEvent.title, date: this.getFormattedDate(), })}`, - html: renderEmail("DailyVideoDownloadRecordingEmail", { + html: await renderEmail("DailyVideoDownloadRecordingEmail", { title: this.calEvent.title, date: this.getFormattedDate(), downloadLink: this.downloadLink, diff --git a/packages/emails/templates/organizer-location-change-email.ts b/packages/emails/templates/organizer-location-change-email.ts index a0ed9e7993..8eb1110e52 100644 --- a/packages/emails/templates/organizer-location-change-email.ts +++ b/packages/emails/templates/organizer-location-change-email.ts @@ -4,7 +4,7 @@ import { renderEmail } from "../"; import OrganizerScheduledEmail from "./organizer-scheduled-email"; export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; return { @@ -20,7 +20,7 @@ export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmai name: this.calEvent.attendees[0].name, date: this.getFormattedDate(), })}`, - html: renderEmail("OrganizerLocationChangeEmail", { + html: await renderEmail("OrganizerLocationChangeEmail", { attendee: this.calEvent.organizer, calEvent: this.calEvent, }), diff --git a/packages/emails/templates/organizer-payment-refund-failed-email.ts b/packages/emails/templates/organizer-payment-refund-failed-email.ts index 26818b1fd7..fd907ba1b5 100644 --- a/packages/emails/templates/organizer-payment-refund-failed-email.ts +++ b/packages/emails/templates/organizer-payment-refund-failed-email.ts @@ -4,7 +4,7 @@ import { renderEmail } from "../"; import OrganizerScheduledEmail from "./organizer-scheduled-email"; export default class OrganizerPaymentRefundFailedEmail extends OrganizerScheduledEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; return { @@ -15,7 +15,7 @@ export default class OrganizerPaymentRefundFailedEmail extends OrganizerSchedule name: this.calEvent.attendees[0].name, date: this.getFormattedDate(), })}`, - html: renderEmail("OrganizerPaymentRefundFailedEmail", { + html: await renderEmail("OrganizerPaymentRefundFailedEmail", { calEvent: this.calEvent, attendee: this.calEvent.organizer, }), diff --git a/packages/emails/templates/organizer-request-email.ts b/packages/emails/templates/organizer-request-email.ts index 0267df2261..47b82627f5 100644 --- a/packages/emails/templates/organizer-request-email.ts +++ b/packages/emails/templates/organizer-request-email.ts @@ -4,7 +4,7 @@ import { renderEmail } from "../"; import OrganizerScheduledEmail from "./organizer-scheduled-email"; export default class OrganizerRequestEmail extends OrganizerScheduledEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; return { @@ -12,7 +12,7 @@ export default class OrganizerRequestEmail extends OrganizerScheduledEmail { to: toAddresses.join(","), replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)], subject: `${this.t("awaiting_approval")}: ${this.calEvent.title}`, - html: renderEmail("OrganizerRequestEmail", { + html: await renderEmail("OrganizerRequestEmail", { calEvent: this.calEvent, attendee: this.calEvent.organizer, }), diff --git a/packages/emails/templates/organizer-request-reminder-email.ts b/packages/emails/templates/organizer-request-reminder-email.ts index b9f2c1b53c..b8d5130a7a 100644 --- a/packages/emails/templates/organizer-request-reminder-email.ts +++ b/packages/emails/templates/organizer-request-reminder-email.ts @@ -4,7 +4,7 @@ import { renderEmail } from "../"; import OrganizerRequestEmail from "./organizer-request-email"; export default class OrganizerRequestReminderEmail extends OrganizerRequestEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; return { @@ -15,7 +15,7 @@ export default class OrganizerRequestReminderEmail extends OrganizerRequestEmail title: this.calEvent.title, date: this.getFormattedDate(), })}`, - html: renderEmail("OrganizerRequestReminderEmail", { + html: await renderEmail("OrganizerRequestReminderEmail", { calEvent: this.calEvent, attendee: this.calEvent.organizer, }), diff --git a/packages/emails/templates/organizer-requested-to-reschedule-email.ts b/packages/emails/templates/organizer-requested-to-reschedule-email.ts index 7a601af696..7f8bdfb2fb 100644 --- a/packages/emails/templates/organizer-requested-to-reschedule-email.ts +++ b/packages/emails/templates/organizer-requested-to-reschedule-email.ts @@ -15,7 +15,7 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu super({ calEvent }); this.metadata = metadata; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { const toAddresses = [this.calEvent.organizer.email]; return { @@ -30,7 +30,7 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu name: this.calEvent.attendees[0].name, date: this.getFormattedDate(), })}`, - html: renderEmail("OrganizerRequestedToRescheduleEmail", { + html: await renderEmail("OrganizerRequestedToRescheduleEmail", { calEvent: this.calEvent, attendee: this.calEvent.organizer, }), diff --git a/packages/emails/templates/organizer-rescheduled-email.ts b/packages/emails/templates/organizer-rescheduled-email.ts index 26da823a1e..9dfa5fe9ba 100644 --- a/packages/emails/templates/organizer-rescheduled-email.ts +++ b/packages/emails/templates/organizer-rescheduled-email.ts @@ -4,7 +4,7 @@ import { renderEmail } from "../"; import OrganizerScheduledEmail from "./organizer-scheduled-email"; export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail { - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; return { @@ -19,7 +19,7 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail { title: this.calEvent.title, date: this.getFormattedDate(), })}`, - html: renderEmail("OrganizerRescheduledEmail", { + html: await renderEmail("OrganizerRescheduledEmail", { calEvent: { ...this.calEvent, attendeeSeatId: undefined }, attendee: this.calEvent.organizer, }), diff --git a/packages/emails/templates/organizer-scheduled-email.ts b/packages/emails/templates/organizer-scheduled-email.ts index 76b036252e..bd1a490e39 100644 --- a/packages/emails/templates/organizer-scheduled-email.ts +++ b/packages/emails/templates/organizer-scheduled-email.ts @@ -70,7 +70,7 @@ export default class OrganizerScheduledEmail extends BaseEmail { return icsEvent.value; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { const clonedCalEvent = cloneDeep(this.calEvent); const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; @@ -83,7 +83,7 @@ export default class OrganizerScheduledEmail extends BaseEmail { to: toAddresses.join(","), replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)], subject: `${this.newSeat ? `${this.t("new_attendee")}: ` : ""}${this.calEvent.title}`, - html: renderEmail("OrganizerScheduledEmail", { + html: await renderEmail("OrganizerScheduledEmail", { calEvent: clonedCalEvent, attendee: this.calEvent.organizer, teamMember: this.teamMember, diff --git a/packages/emails/templates/slug-replacement-email.ts b/packages/emails/templates/slug-replacement-email.ts index c9316cb28d..2a2655718d 100644 --- a/packages/emails/templates/slug-replacement-email.ts +++ b/packages/emails/templates/slug-replacement-email.ts @@ -19,12 +19,12 @@ export default class SlugReplacementEmail extends BaseEmail { this.t = t; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { from: `Cal.com <${this.getMailerOptions().from}>`, to: this.email, subject: this.t("email_subject_slug_replacement", { slug: this.slug }), - html: renderEmail("SlugReplacementEmail", { + html: await renderEmail("SlugReplacementEmail", { slug: this.slug, name: this.name, teamName: this.teamName || "", diff --git a/packages/emails/templates/team-invite-email.ts b/packages/emails/templates/team-invite-email.ts index d583f7dc96..6d272bd225 100644 --- a/packages/emails/templates/team-invite-email.ts +++ b/packages/emails/templates/team-invite-email.ts @@ -24,7 +24,7 @@ export default class TeamInviteEmail extends BaseEmail { this.teamInviteEvent = teamInviteEvent; } - protected getNodeMailerPayload(): Record { + protected async getNodeMailerPayload(): Promise> { return { to: this.teamInviteEvent.to, from: `${APP_NAME} <${this.getMailerOptions().from}>`, @@ -36,7 +36,7 @@ export default class TeamInviteEmail extends BaseEmail { .language(this.teamInviteEvent.isOrg ? "organization" : "team") .toLowerCase(), }), - html: renderEmail("TeamInviteEmail", this.teamInviteEvent), + html: await renderEmail("TeamInviteEmail", this.teamInviteEvent), text: "", }; } diff --git a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts index 622b88e950..fca448bf2d 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts @@ -37,7 +37,7 @@ import { expectBookingRequestedWebhookToHaveBeenFired, expectSuccessfulCalendarEventDeletionInCalendar, expectSuccessfulVideoMeetingDeletionInCalendar, - expectSuccessfulRoudRobinReschedulingEmails, + expectSuccessfulRoundRobinReschedulingEmails, } from "@calcom/web/test/utils/bookingScenario/expects"; import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; @@ -1692,7 +1692,7 @@ describe("handleNewBooking", () => { }, }); - expectSuccessfulRoudRobinReschedulingEmails({ + expectSuccessfulRoundRobinReschedulingEmails({ prevOrganizer: roundRobinHost1, newOrganizer: roundRobinHost2, emails, @@ -1842,7 +1842,7 @@ describe("handleNewBooking", () => { }, }); - expectSuccessfulRoudRobinReschedulingEmails({ + expectSuccessfulRoundRobinReschedulingEmails({ prevOrganizer: roundRobinHost1, newOrganizer: roundRobinHost1, // Round robin host 2 is not available and it will be rescheduled to same user emails, diff --git a/packages/features/filters/lib/getTeamsFiltersFromQuery.ts b/packages/features/filters/lib/getTeamsFiltersFromQuery.ts index 14d71c9d78..f0c1aad074 100644 --- a/packages/features/filters/lib/getTeamsFiltersFromQuery.ts +++ b/packages/features/filters/lib/getTeamsFiltersFromQuery.ts @@ -1,12 +1,24 @@ +"use client"; + import type { ParsedUrlQuery } from "querystring"; import { z } from "zod"; -import { queryNumberArray } from "@calcom/lib/hooks/useTypedQuery"; import type { RouterOutputs } from "@calcom/trpc/react"; export type IEventTypesFilters = RouterOutputs["viewer"]["eventTypes"]["listWithTeam"]; export type IEventTypeFilter = IEventTypesFilters[0]; +// Take array as a string and return zod array +const queryNumberArray = z + .string() + .or(z.number()) + .or(z.array(z.number())) + .transform((a) => { + if (typeof a === "string") return a.split(",").map((a) => Number(a)); + if (Array.isArray(a)) return a; + return [a]; + }); + // Use filterQuerySchema when parsing filters out of query, so that additional query params(e.g. slug, appPages) aren't passed in filters export const filterQuerySchema = z.object({ teamIds: queryNumberArray.optional(), diff --git a/packages/features/settings/layouts/SettingsLayoutAppDir.tsx b/packages/features/settings/layouts/SettingsLayoutAppDir.tsx new file mode 100644 index 0000000000..7719211f1b --- /dev/null +++ b/packages/features/settings/layouts/SettingsLayoutAppDir.tsx @@ -0,0 +1,723 @@ +"use client"; + +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; +import { useSession } from "next-auth/react"; +import Link from "next/link"; +import { usePathname, useRouter } from "next/navigation"; +import type { ComponentProps } from "react"; +import React, { Suspense, useEffect, useState } from "react"; + +import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; +import Shell from "@calcom/features/shell/Shell"; +import { classNames } from "@calcom/lib"; +import { HOSTED_CAL_FEATURES, WEBAPP_URL } from "@calcom/lib/constants"; +import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; +import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { IdentityProvider, MembershipRole, UserPermissionRole } from "@calcom/prisma/enums"; +import { trpc } from "@calcom/trpc/react"; +import type { VerticalTabItemProps } from "@calcom/ui"; +import { Badge, Button, ErrorBoundary, Skeleton, useMeta, VerticalTabItem } from "@calcom/ui"; +import { + ArrowLeft, + ChevronDown, + ChevronRight, + CreditCard, + Key, + Loader, + Lock, + Menu, + Plus, + Terminal, + User, + Users, +} from "@calcom/ui/components/icon"; + +const tabs: VerticalTabItemProps[] = [ + { + name: "my_account", + href: "/settings/my-account", + icon: User, + children: [ + { name: "profile", href: "/settings/my-account/profile" }, + { name: "general", href: "/settings/my-account/general" }, + { name: "calendars", href: "/settings/my-account/calendars" }, + { name: "conferencing", href: "/settings/my-account/conferencing" }, + { name: "appearance", href: "/settings/my-account/appearance" }, + // TODO + // { name: "referrals", href: "/settings/my-account/referrals" }, + ], + }, + { + name: "security", + href: "/settings/security", + icon: Key, + children: [ + { name: "password", href: "/settings/security/password" }, + { name: "impersonation", href: "/settings/security/impersonation" }, + { name: "2fa_auth", href: "/settings/security/two-factor-auth" }, + ], + }, + { + name: "billing", + href: "/settings/billing", + icon: CreditCard, + children: [{ name: "manage_billing", href: "/settings/billing" }], + }, + { + name: "developer", + href: "/settings/developer", + icon: Terminal, + children: [ + // + { name: "webhooks", href: "/settings/developer/webhooks" }, + { name: "api_keys", href: "/settings/developer/api-keys" }, + // TODO: Add profile level for embeds + // { name: "embeds", href: "/v2/settings/developer/embeds" }, + ], + }, + { + name: "organization", + href: "/settings/organizations", + children: [ + { + name: "profile", + href: "/settings/organizations/profile", + }, + { + name: "general", + href: "/settings/organizations/general", + }, + { + name: "members", + href: "/settings/organizations/members", + }, + { + name: "appearance", + href: "/settings/organizations/appearance", + }, + { + name: "billing", + href: "/settings/organizations/billing", + }, + ], + }, + { + name: "teams", + href: "/settings/teams", + icon: Users, + children: [], + }, + { + name: "admin", + href: "/settings/admin", + icon: Lock, + children: [ + // + { name: "features", href: "/settings/admin/flags" }, + { name: "license", href: "/auth/setup?step=1" }, + { name: "impersonation", href: "/settings/admin/impersonation" }, + { name: "apps", href: "/settings/admin/apps/calendar" }, + { name: "users", href: "/settings/admin/users" }, + { name: "organizations", href: "/settings/admin/organizations" }, + { name: "oAuth", href: "/settings/admin/oAuth" }, + ], + }, +]; + +tabs.find((tab) => { + // Add "SAML SSO" to the tab + if (tab.name === "security" && !HOSTED_CAL_FEATURES) { + tab.children?.push({ name: "sso_configuration", href: "/settings/security/sso" }); + } +}); + +// The following keys are assigned to admin only +const adminRequiredKeys = ["admin"]; +const organizationRequiredKeys = ["organization"]; + +const useTabs = () => { + const session = useSession(); + const { data: user } = trpc.viewer.me.useQuery(); + const orgBranding = useOrgBranding(); + + const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN; + + tabs.map((tab) => { + if (tab.href === "/settings/my-account") { + tab.name = user?.name || "my_account"; + tab.icon = undefined; + tab.avatar = getUserAvatarUrl(user); + } else if (tab.href === "/settings/organizations") { + tab.name = orgBranding?.name || "organization"; + tab.avatar = `${orgBranding?.fullDomain}/org/${orgBranding?.slug}/avatar.png`; + } else if ( + tab.href === "/settings/security" && + user?.identityProvider === IdentityProvider.GOOGLE && + !user?.twoFactorEnabled + ) { + tab.children = tab?.children?.filter( + (childTab) => childTab.href !== "/settings/security/two-factor-auth" + ); + } + return tab; + }); + + // check if name is in adminRequiredKeys + return tabs.filter((tab) => { + if (organizationRequiredKeys.includes(tab.name)) return !!session.data?.user?.org; + + if (isAdmin) return true; + return !adminRequiredKeys.includes(tab.name); + }); +}; + +const BackButtonInSidebar = ({ name }: { name: string }) => { + return ( + + + + {name} + + + ); +}; + +interface SettingsSidebarContainerProps { + className?: string; + navigationIsOpenedOnMobile?: boolean; + bannersHeight?: number; +} + +const SettingsSidebarContainer = ({ + className = "", + navigationIsOpenedOnMobile, + bannersHeight, +}: SettingsSidebarContainerProps) => { + const searchParams = useCompatSearchParams(); + const { t } = useLocale(); + const tabsWithPermissions = useTabs(); + const [teamMenuState, setTeamMenuState] = + useState<{ teamId: number | undefined; teamMenuOpen: boolean }[]>(); + const [otherTeamMenuState, setOtherTeamMenuState] = useState< + { + teamId: number | undefined; + teamMenuOpen: boolean; + }[] + >(); + const { data: teams } = trpc.viewer.teams.list.useQuery(); + const session = useSession(); + const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { + enabled: !!session.data?.user?.org, + }); + + const { data: otherTeams } = trpc.viewer.organizations.listOtherTeams.useQuery(); + + useEffect(() => { + if (teams) { + const teamStates = teams?.map((team) => ({ + teamId: team.id, + teamMenuOpen: String(team.id) === searchParams?.get("id"), + })); + setTeamMenuState(teamStates); + setTimeout(() => { + const tabMembers = Array.from(document.getElementsByTagName("a")).filter( + (bottom) => bottom.dataset.testid === "vertical-tab-Members" + )[1]; + tabMembers?.scrollIntoView({ behavior: "smooth" }); + }, 100); + } + }, [searchParams?.get("id"), teams]); + + // Same as above but for otherTeams + useEffect(() => { + if (otherTeams) { + const otherTeamStates = otherTeams?.map((team) => ({ + teamId: team.id, + teamMenuOpen: String(team.id) === searchParams?.get("id"), + })); + setOtherTeamMenuState(otherTeamStates); + setTimeout(() => { + // @TODO: test if this works for 2 dataset testids + const tabMembers = Array.from(document.getElementsByTagName("a")).filter( + (bottom) => bottom.dataset.testid === "vertical-tab-Members" + )[1]; + tabMembers?.scrollIntoView({ behavior: "smooth" }); + }, 100); + } + }, [searchParams?.get("id"), otherTeams]); + + const isOrgAdminOrOwner = + currentOrg && currentOrg?.user?.role && ["OWNER", "ADMIN"].includes(currentOrg?.user?.role); + + if (isOrgAdminOrOwner) { + const teamsIndex = tabsWithPermissions.findIndex((tab) => tab.name === "teams"); + + tabsWithPermissions.splice(teamsIndex + 1, 0, { + name: "other_teams", + href: "/settings/organizations/teams/other", + icon: Users, + children: [], + }); + } + + return ( + + ); +}; + +const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) => { + const { t } = useLocale(); + const router = useRouter(); + + return ( + <> + + + ); +}; + +export default function SettingsLayout({ + children, + ...rest +}: { children: React.ReactNode } & ComponentProps) { + const pathname = usePathname(); + const state = useState(false); + const { t } = useLocale(); + const [sideContainerOpen, setSideContainerOpen] = state; + + useEffect(() => { + const closeSideContainer = () => { + if (window.innerWidth >= 1024) { + setSideContainerOpen(false); + } + }; + + window.addEventListener("resize", closeSideContainer); + return () => { + window.removeEventListener("resize", closeSideContainer); + }; + }, []); + + useEffect(() => { + if (sideContainerOpen) { + setSideContainerOpen(!sideContainerOpen); + } + }, [pathname]); + + return ( + + } + drawerState={state} + MobileNavigationContainer={null} + TopNavContainer={ + setSideContainerOpen(!sideContainerOpen)} /> + }> +
+
+ + + }>{children} + +
+
+
+ ); +} + +const SidebarContainerElement = ({ + sideContainerOpen, + bannersHeight, + setSideContainerOpen, +}: SidebarContainerElementProps) => { + const { t } = useLocale(); + return ( + <> + {/* Mobile backdrop */} + {sideContainerOpen && ( + + )} + + + ); +}; + +type SidebarContainerElementProps = { + sideContainerOpen: boolean; + bannersHeight?: number; + setSideContainerOpen: React.Dispatch>; +}; + +export const getLayout = (page: React.ReactElement) => {page}; + +export function ShellHeader() { + const { meta } = useMeta(); + const { t, isLocaleReady } = useLocale(); + return ( + <> +
+
+ {meta.backButton && ( + + + + )} +
+ {meta.title && isLocaleReady ? ( +

+ {t(meta.title)} +

+ ) : ( +
+ )} + {meta.description && isLocaleReady ? ( +

{t(meta.description)}

+ ) : ( +
+ )} +
+
{meta.CTA}
+
+
+ + ); +} diff --git a/packages/lib/hooks/useTypedQuery.ts b/packages/lib/hooks/useTypedQuery.ts index 974c0ede25..4d51dd2d97 100644 --- a/packages/lib/hooks/useTypedQuery.ts +++ b/packages/lib/hooks/useTypedQuery.ts @@ -1,3 +1,5 @@ +"use client"; + import { usePathname, useRouter } from "next/navigation"; import { useCallback, useMemo, useEffect } from "react"; import { z } from "zod"; diff --git a/packages/trpc/react/shared.ts b/packages/trpc/react/shared.ts index 0cc50c7959..14598e7011 100644 --- a/packages/trpc/react/shared.ts +++ b/packages/trpc/react/shared.ts @@ -1 +1,29 @@ export * from "@trpc/react-query/shared"; + +export const ENDPOINTS = [ + "admin", + "apiKeys", + "appRoutingForms", + "apps", + "auth", + "availability", + "appBasecamp3", + "bookings", + "deploymentSetup", + "eventTypes", + "features", + "insights", + "payments", + "public", + "saml", + "slots", + "teams", + "organizations", + "users", + "viewer", + "webhook", + "workflows", + "appsRouter", + "googleWorkspace", + "oAuth", +] as const; diff --git a/packages/trpc/react/trpc.ts b/packages/trpc/react/trpc.ts index 6b34bfb2b3..3d1167b775 100644 --- a/packages/trpc/react/trpc.ts +++ b/packages/trpc/react/trpc.ts @@ -11,38 +11,13 @@ import { createTRPCNext } from "../next"; import type { TRPCClientErrorLike } from "../react"; import type { inferRouterInputs, inferRouterOutputs, Maybe } from "../server"; import type { AppRouter } from "../server/routers/_app"; +import { ENDPOINTS } from "./shared"; /** * We deploy our tRPC router on multiple lambdas to keep number of imports as small as possible * TODO: Make this dynamic based on folders in trpc server? */ -const ENDPOINTS = [ - "admin", - "apiKeys", - "appRoutingForms", - "apps", - "auth", - "availability", - "appBasecamp3", - "bookings", - "deploymentSetup", - "eventTypes", - "features", - "insights", - "payments", - "public", - "saml", - "slots", - "teams", - "organizations", - "users", - "viewer", - "webhook", - "workflows", - "appsRouter", - "googleWorkspace", - "oAuth", -] as const; + export type Endpoint = (typeof ENDPOINTS)[number]; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/trpc/server/routers/viewer/workflows/filteredList.schema.tsx b/packages/trpc/server/routers/viewer/workflows/filteredList.schema.tsx index 3bb74fbb1f..0f28959ada 100644 --- a/packages/trpc/server/routers/viewer/workflows/filteredList.schema.tsx +++ b/packages/trpc/server/routers/viewer/workflows/filteredList.schema.tsx @@ -1,3 +1,5 @@ +"use client"; + import { z } from "zod"; import { filterQuerySchemaStrict } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery"; diff --git a/turbo.json b/turbo.json index 5fabef8bca..ab654d12ba 100644 --- a/turbo.json +++ b/turbo.json @@ -199,6 +199,7 @@ "ANALYZE", "API_KEY_PREFIX", "APP_ROUTER_EVENT_TYPES_ENABLED", + "APP_ROUTER_SETTINGS_ADMIN_ENABLED", "APP_USER_NAME", "BASECAMP3_CLIENT_ID", "BASECAMP3_CLIENT_SECRET",