chore: [app-router-migration-1] migrate the pages in `settings/admin` to the app directory (#12561)
Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com> Co-authored-by: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
parent
d13dedda9a
commit
ca78be011c
|
@ -290,3 +290,4 @@ E2E_TEST_OIDC_USER_PASSWORD=
|
||||||
AB_TEST_BUCKET_PROBABILITY=50
|
AB_TEST_BUCKET_PROBABILITY=50
|
||||||
# whether we redirect to the future/event-types from event-types or not
|
# whether we redirect to the future/event-types from event-types or not
|
||||||
APP_ROUTER_EVENT_TYPES_ENABLED=1
|
APP_ROUTER_EVENT_TYPES_ENABLED=1
|
||||||
|
APP_ROUTER_SETTINGS_ADMIN_ENABLED=1
|
|
@ -24,6 +24,8 @@ runs:
|
||||||
**/.turbo/**
|
**/.turbo/**
|
||||||
**/dist/**
|
**/dist/**
|
||||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
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'
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
|
@ -5,6 +5,7 @@ import z from "zod";
|
||||||
|
|
||||||
const ROUTES: [URLPattern, boolean][] = [
|
const ROUTES: [URLPattern, boolean][] = [
|
||||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
["/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]) => [
|
].map(([pathname, enabled]) => [
|
||||||
new URLPattern({
|
new URLPattern({
|
||||||
pathname,
|
pathname,
|
||||||
|
@ -27,7 +28,6 @@ export const abTestMiddlewareFactory =
|
||||||
const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME);
|
const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME);
|
||||||
|
|
||||||
const route = ROUTES.find(([regExp]) => regExp.test(req.url)) ?? null;
|
const route = ROUTES.find(([regExp]) => regExp.test(req.url)) ?? null;
|
||||||
|
|
||||||
const enabled = route !== null ? route[1] || override : false;
|
const enabled = route !== null ? route[1] || override : false;
|
||||||
|
|
||||||
if (pathname.includes("future") || !enabled) {
|
if (pathname.includes("future") || !enabled) {
|
||||||
|
|
|
@ -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);
|
|
@ -8,33 +8,8 @@ import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
|
||||||
import { httpLink } from "@calcom/trpc/client/links/httpLink";
|
import { httpLink } from "@calcom/trpc/client/links/httpLink";
|
||||||
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
|
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
|
||||||
import { splitLink } from "@calcom/trpc/client/links/splitLink";
|
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];
|
export type Endpoint = (typeof ENDPOINTS)[number];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export type Params = {
|
||||||
|
[param: string]: string | string[] | undefined;
|
||||||
|
};
|
|
@ -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 (
|
||||||
|
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
|
||||||
|
{children}
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 (
|
||||||
|
<PageWrapper getLayout={null} requiresLicense={false} nonce={nonce} themeBasis={null}>
|
||||||
|
{children}
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
|
@ -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 (
|
||||||
|
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
|
||||||
|
{children}
|
||||||
|
</PageWrapper>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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<typeof Shell>) {
|
||||||
|
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 (
|
||||||
|
<SettingsLayout {...rest}>
|
||||||
|
<div className="divide-subtle mx-auto flex max-w-4xl flex-row divide-y">
|
||||||
|
<div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
|
||||||
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLayout = (page: React.ReactElement) => <AdminLayout>{page}</AdminLayout>;
|
|
@ -101,6 +101,8 @@ export const config = {
|
||||||
"/apps/routing_forms/:path*",
|
"/apps/routing_forms/:path*",
|
||||||
"/event-types",
|
"/event-types",
|
||||||
"/future/event-types/",
|
"/future/event-types/",
|
||||||
|
"/settings/admin/:path*",
|
||||||
|
"/future/settings/admin/:path*",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -231,6 +231,9 @@ const nextConfig = {
|
||||||
...config.resolve.fallback, // if you miss it, all the other options in fallback, specified
|
...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
|
// by next.js will be dropped. Doesn't make much sense, but how it is
|
||||||
fs: false,
|
fs: false,
|
||||||
|
// ignore module resolve errors caused by the server component bundler
|
||||||
|
"pg-native": false,
|
||||||
|
"superagent-proxy": false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -13,7 +13,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
res.setHeader("Content-Type", "text/html");
|
res.setHeader("Content-Type", "text/html");
|
||||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||||
res.write(
|
res.write(
|
||||||
renderEmail("MonthlyDigestEmail", {
|
await renderEmail("MonthlyDigestEmail", {
|
||||||
language: t,
|
language: t,
|
||||||
Created: 12,
|
Created: 12,
|
||||||
Completed: 13,
|
Completed: 13,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Meta } from "@calcom/ui";
|
import { Meta } from "@calcom/ui";
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
|
"use client";
|
||||||
export { default } from "./[category]";
|
export { default } from "./[category]";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { FlagListingView } from "@calcom/features/flags/pages/flag-listing-view";
|
import { FlagListingView } from "@calcom/features/flags/pages/flag-listing-view";
|
||||||
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { Meta } from "@calcom/ui";
|
import { Meta } from "@calcom/ui";
|
||||||
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import AdminOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
|
import AdminOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
|
||||||
|
|
||||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import UsersEditView from "@calcom/features/ee/users/pages/users-edit-view";
|
import UsersEditView from "@calcom/features/ee/users/pages/users-edit-view";
|
||||||
|
|
||||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import UsersAddView from "@calcom/features/ee/users/pages/users-add-view";
|
import UsersAddView from "@calcom/features/ee/users/pages/users-add-view";
|
||||||
|
|
||||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import UsersListingView from "@calcom/features/ee/users/pages/users-listing-view";
|
import UsersListingView from "@calcom/features/ee/users/pages/users-listing-view";
|
||||||
|
|
||||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,7 +4,7 @@ import type { WebhookTriggerEvents, Booking, BookingReference, DestinationCalend
|
||||||
import { parse } from "node-html-parser";
|
import { parse } from "node-html-parser";
|
||||||
import type { VEvent } from "node-ical";
|
import type { VEvent } from "node-ical";
|
||||||
import ical from "node-ical";
|
import ical from "node-ical";
|
||||||
import { expect } from "vitest";
|
import { expect, vi } from "vitest";
|
||||||
import "vitest-fetch-mock";
|
import "vitest-fetch-mock";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
@ -547,7 +547,7 @@ export function expectCalendarEventCreationFailureEmails({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function expectSuccessfulRoudRobinReschedulingEmails({
|
export function expectSuccessfulRoundRobinReschedulingEmails({
|
||||||
emails,
|
emails,
|
||||||
newOrganizer,
|
newOrganizer,
|
||||||
prevOrganizer,
|
prevOrganizer,
|
||||||
|
@ -557,32 +557,38 @@ export function expectSuccessfulRoudRobinReschedulingEmails({
|
||||||
prevOrganizer: { email: string; name: string };
|
prevOrganizer: { email: string; name: string };
|
||||||
}) {
|
}) {
|
||||||
if (newOrganizer !== prevOrganizer) {
|
if (newOrganizer !== prevOrganizer) {
|
||||||
// new organizer should recieve scheduling emails
|
vi.waitFor(() => {
|
||||||
expect(emails).toHaveEmail(
|
// new organizer should recieve scheduling emails
|
||||||
{
|
expect(emails).toHaveEmail(
|
||||||
heading: "new_event_scheduled",
|
{
|
||||||
to: `${newOrganizer.email}`,
|
heading: "new_event_scheduled",
|
||||||
},
|
to: `${newOrganizer.email}`,
|
||||||
`${newOrganizer.email}`
|
},
|
||||||
);
|
`${newOrganizer.email}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// old organizer should recieve cancelled emails
|
vi.waitFor(() => {
|
||||||
expect(emails).toHaveEmail(
|
// old organizer should recieve cancelled emails
|
||||||
{
|
expect(emails).toHaveEmail(
|
||||||
heading: "event_request_cancelled",
|
{
|
||||||
to: `${prevOrganizer.email}`,
|
heading: "event_request_cancelled",
|
||||||
},
|
to: `${prevOrganizer.email}`,
|
||||||
`${prevOrganizer.email}`
|
},
|
||||||
);
|
`${prevOrganizer.email}`
|
||||||
|
);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// organizer should recieve rescheduled emails
|
vi.waitFor(() => {
|
||||||
expect(emails).toHaveEmail(
|
// organizer should recieve rescheduled emails
|
||||||
{
|
expect(emails).toHaveEmail(
|
||||||
heading: "event_has_been_rescheduled",
|
{
|
||||||
to: `${newOrganizer.email}`,
|
heading: "event_has_been_rescheduled",
|
||||||
},
|
to: `${newOrganizer.email}`,
|
||||||
`${newOrganizer.email}`
|
},
|
||||||
);
|
`${newOrganizer.email}`
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@
|
||||||
"prismock": "^1.21.1",
|
"prismock": "^1.21.1",
|
||||||
"tsc-absolute": "^1.0.0",
|
"tsc-absolute": "^1.0.0",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.4",
|
||||||
"vitest": "^0.34.3",
|
"vitest": "^0.34.6",
|
||||||
"vitest-fetch-mock": "^0.2.2",
|
"vitest-fetch-mock": "^0.2.2",
|
||||||
"vitest-mock-extended": "^1.1.3"
|
"vitest-mock-extended": "^1.1.3"
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,14 +25,14 @@ export default class ResponseEmail extends BaseEmail {
|
||||||
this.toAddresses = toAddresses;
|
this.toAddresses = toAddresses;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const toAddresses = this.toAddresses;
|
const toAddresses = this.toAddresses;
|
||||||
const subject = `${this.form.name} has a new response`;
|
const subject = `${this.form.name} has a new response`;
|
||||||
return {
|
return {
|
||||||
from: `Cal.com <${this.getMailerOptions().from}>`,
|
from: `Cal.com <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
subject,
|
subject,
|
||||||
html: renderEmail("ResponseEmail", {
|
html: await renderEmail("ResponseEmail", {
|
||||||
form: this.form,
|
form: this.form,
|
||||||
orderedResponses: this.orderedResponses,
|
orderedResponses: this.orderedResponses,
|
||||||
subject,
|
subject,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { filterQuerySchemaStrict } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
import { filterQuerySchemaStrict } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import * as ReactDOMServer from "react-dom/server";
|
|
||||||
|
|
||||||
import * as templates from "./templates";
|
import * as templates from "./templates";
|
||||||
|
|
||||||
function renderEmail<K extends keyof typeof templates>(
|
async function renderEmail<K extends keyof typeof templates>(
|
||||||
template: K,
|
template: K,
|
||||||
props: React.ComponentProps<(typeof templates)[K]>
|
props: React.ComponentProps<(typeof templates)[K]>
|
||||||
) {
|
) {
|
||||||
const Component = templates[template];
|
const Component = templates[template];
|
||||||
|
const ReactDOMServer = (await import("react-dom/server")).default;
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
|
|
|
@ -24,7 +24,7 @@ export default class BaseEmail {
|
||||||
return dayjs(time).tz(this.getTimezone()).locale(this.getLocale()).format(format);
|
return dayjs(time).tz(this.getTimezone()).locale(this.getLocale()).format(format);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
public async sendEmail() {
|
public async sendEmail() {
|
||||||
|
@ -38,21 +38,20 @@ export default class BaseEmail {
|
||||||
if (process.env.INTEGRATION_TEST_MODE === "true") {
|
if (process.env.INTEGRATION_TEST_MODE === "true") {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
//@ts-expect-error
|
//@ts-expect-error
|
||||||
setTestEmail(this.getNodeMailerPayload());
|
setTestEmail(await this.getNodeMailerPayload());
|
||||||
console.log(
|
console.log(
|
||||||
"Skipped Sending Email as process.env.NEXT_PUBLIC_UNIT_TESTS is set. Emails are available in globalThis.testEmails"
|
"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"));
|
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 parseSubject = z.string().safeParse(payload?.subject);
|
||||||
const payloadWithUnEscapedSubject = {
|
const payloadWithUnEscapedSubject = {
|
||||||
headers: this.getMailerOptions().headers,
|
headers: this.getMailerOptions().headers,
|
||||||
...payload,
|
...payload,
|
||||||
...(parseSubject.success && { subject: decodeHTML(parseSubject.data) }),
|
...(parseSubject.success && { subject: decodeHTML(parseSubject.data) }),
|
||||||
};
|
};
|
||||||
|
|
||||||
await new Promise((resolve, reject) =>
|
await new Promise((resolve, reject) =>
|
||||||
createTransport(this.getMailerOptions().transport).sendMail(
|
createTransport(this.getMailerOptions().transport).sendMail(
|
||||||
payloadWithUnEscapedSubject,
|
payloadWithUnEscapedSubject,
|
||||||
|
@ -69,7 +68,6 @@ export default class BaseEmail {
|
||||||
).catch((e) => console.error("sendEmail", e));
|
).catch((e) => console.error("sendEmail", e));
|
||||||
return new Promise((resolve) => resolve("send mail async"));
|
return new Promise((resolve) => resolve("send mail async"));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getMailerOptions() {
|
protected getMailerOptions() {
|
||||||
return {
|
return {
|
||||||
transport: serverConfig.transport,
|
transport: serverConfig.transport,
|
||||||
|
@ -77,7 +75,6 @@ export default class BaseEmail {
|
||||||
headers: serverConfig.headers,
|
headers: serverConfig.headers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected printNodeMailerError(error: Error): void {
|
protected printNodeMailerError(error: Error): void {
|
||||||
/** Don't clog the logs with unsent emails in E2E */
|
/** Don't clog the logs with unsent emails in E2E */
|
||||||
if (process.env.NEXT_PUBLIC_IS_E2E) return;
|
if (process.env.NEXT_PUBLIC_IS_E2E) return;
|
||||||
|
|
|
@ -23,14 +23,14 @@ export default class AccountVerifyEmail extends BaseEmail {
|
||||||
this.verifyAccountInput = passwordEvent;
|
this.verifyAccountInput = passwordEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
to: `${this.verifyAccountInput.user.name} <${this.verifyAccountInput.user.email}>`,
|
to: `${this.verifyAccountInput.user.name} <${this.verifyAccountInput.user.email}>`,
|
||||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||||
subject: this.verifyAccountInput.language("verify_email_subject", {
|
subject: this.verifyAccountInput.language("verify_email_subject", {
|
||||||
appName: APP_NAME,
|
appName: APP_NAME,
|
||||||
}),
|
}),
|
||||||
html: renderEmail("VerifyAccountEmail", this.verifyAccountInput),
|
html: await renderEmail("VerifyAccountEmail", this.verifyAccountInput),
|
||||||
text: this.getTextBody(),
|
text: this.getTextBody(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,12 @@ export default class AdminOrganizationNotification extends BaseEmail {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: this.input.instanceAdmins.map((admin) => admin.email).join(","),
|
to: this.input.instanceAdmins.map((admin) => admin.email).join(","),
|
||||||
subject: `${this.input.t("admin_org_notification_email_subject")}`,
|
subject: `${this.input.t("admin_org_notification_email_subject")}`,
|
||||||
html: renderEmail("AdminOrganizationNotificationEmail", {
|
html: await renderEmail("AdminOrganizationNotificationEmail", {
|
||||||
orgSlug: this.input.orgSlug,
|
orgSlug: this.input.orgSlug,
|
||||||
webappIPAddress: this.input.webappIPAddress,
|
webappIPAddress: this.input.webappIPAddress,
|
||||||
language: this.input.t,
|
language: this.input.t,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { renderEmail } from "../";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
|
|
||||||
export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail {
|
export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
|
@ -11,7 +11,7 @@ export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("AttendeeAwaitingPaymentEmail", {
|
html: await renderEmail("AttendeeAwaitingPaymentEmail", {
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
attendee: this.attendee,
|
attendee: this.attendee,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { renderEmail } from "../";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
|
|
||||||
export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
|
export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
|
@ -11,7 +11,7 @@ export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("AttendeeCancelledEmail", {
|
html: await renderEmail("AttendeeCancelledEmail", {
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
attendee: this.attendee,
|
attendee: this.attendee,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { renderEmail } from "../";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
|
|
||||||
export default class AttendeeCancelledSeatEmail extends AttendeeScheduledEmail {
|
export default class AttendeeCancelledSeatEmail extends AttendeeScheduledEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
|
@ -11,7 +11,7 @@ export default class AttendeeCancelledSeatEmail extends AttendeeScheduledEmail {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("AttendeeCancelledSeatEmail", {
|
html: await renderEmail("AttendeeCancelledSeatEmail", {
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
attendee: this.attendee,
|
attendee: this.attendee,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -21,7 +21,7 @@ export default class AttendeeDailyVideoDownloadRecordingEmail extends BaseEmail
|
||||||
this.downloadLink = downloadLink;
|
this.downloadLink = downloadLink;
|
||||||
this.t = attendee.language.translate;
|
this.t = attendee.language.translate;
|
||||||
}
|
}
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
|
@ -30,7 +30,7 @@ export default class AttendeeDailyVideoDownloadRecordingEmail extends BaseEmail
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("DailyVideoDownloadRecordingEmail", {
|
html: await renderEmail("DailyVideoDownloadRecordingEmail", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
downloadLink: this.downloadLink,
|
downloadLink: this.downloadLink,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { renderEmail } from "../";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
|
|
||||||
export default class AttendeeDeclinedEmail extends AttendeeScheduledEmail {
|
export default class AttendeeDeclinedEmail extends AttendeeScheduledEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
|
@ -11,7 +11,7 @@ export default class AttendeeDeclinedEmail extends AttendeeScheduledEmail {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("AttendeeDeclinedEmail", {
|
html: await renderEmail("AttendeeDeclinedEmail", {
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
attendee: this.attendee,
|
attendee: this.attendee,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { renderEmail } from "../";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
|
|
||||||
export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail {
|
export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
icalEvent: {
|
icalEvent: {
|
||||||
filename: "event.ics",
|
filename: "event.ics",
|
||||||
|
@ -16,7 +16,7 @@ export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail
|
||||||
name: this.calEvent.team?.name || this.calEvent.organizer.name,
|
name: this.calEvent.team?.name || this.calEvent.organizer.name,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("AttendeeLocationChangeEmail", {
|
html: await renderEmail("AttendeeLocationChangeEmail", {
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
attendee: this.attendee,
|
attendee: this.attendee,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { renderEmail } from "../";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
|
|
||||||
export default class AttendeeRequestEmail extends AttendeeScheduledEmail {
|
export default class AttendeeRequestEmail extends AttendeeScheduledEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const toAddresses = this.calEvent.attendees.map((attendee) => attendee.email);
|
const toAddresses = this.calEvent.attendees.map((attendee) => attendee.email);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -15,7 +15,7 @@ export default class AttendeeRequestEmail extends AttendeeScheduledEmail {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("AttendeeRequestEmail", {
|
html: await renderEmail("AttendeeRequestEmail", {
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
attendee: this.attendee,
|
attendee: this.attendee,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { renderEmail } from "../";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
|
|
||||||
export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
|
export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
icalEvent: {
|
icalEvent: {
|
||||||
filename: "event.ics",
|
filename: "event.ics",
|
||||||
|
@ -15,7 +15,7 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("AttendeeRescheduledEmail", {
|
html: await renderEmail("AttendeeRescheduledEmail", {
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
attendee: this.attendee,
|
attendee: this.attendee,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -82,7 +82,7 @@ export default class AttendeeScheduledEmail extends BaseEmail {
|
||||||
return icsEvent.value;
|
return icsEvent.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const clonedCalEvent = cloneDeep(this.calEvent);
|
const clonedCalEvent = cloneDeep(this.calEvent);
|
||||||
|
|
||||||
this.getiCalEventAsString();
|
this.getiCalEventAsString();
|
||||||
|
@ -97,7 +97,7 @@ export default class AttendeeScheduledEmail extends BaseEmail {
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: [...this.calEvent.attendees.map(({ email }) => email), this.calEvent.organizer.email],
|
replyTo: [...this.calEvent.attendees.map(({ email }) => email), this.calEvent.organizer.email],
|
||||||
subject: `${this.calEvent.title}`,
|
subject: `${this.calEvent.title}`,
|
||||||
html: renderEmail("AttendeeScheduledEmail", {
|
html: await renderEmail("AttendeeScheduledEmail", {
|
||||||
calEvent: clonedCalEvent,
|
calEvent: clonedCalEvent,
|
||||||
attendee: this.attendee,
|
attendee: this.attendee,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -23,14 +23,14 @@ export default class AttendeeVerifyEmail extends BaseEmail {
|
||||||
this.verifyAccountInput = passwordEvent;
|
this.verifyAccountInput = passwordEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
to: `${this.verifyAccountInput.user.name} <${this.verifyAccountInput.user.email}>`,
|
to: `${this.verifyAccountInput.user.name} <${this.verifyAccountInput.user.email}>`,
|
||||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||||
subject: this.verifyAccountInput.language("verify_email_subject", {
|
subject: this.verifyAccountInput.language("verify_email_subject", {
|
||||||
appName: APP_NAME,
|
appName: APP_NAME,
|
||||||
}),
|
}),
|
||||||
html: renderEmail("VerifyEmailByCode", this.verifyAccountInput),
|
html: await renderEmail("VerifyEmailByCode", this.verifyAccountInput),
|
||||||
text: this.getTextBody(),
|
text: this.getTextBody(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
|
||||||
this.metadata = metadata;
|
this.metadata = metadata;
|
||||||
this.t = this.calEvent.attendees[0].language.translate;
|
this.t = this.calEvent.attendees[0].language.translate;
|
||||||
}
|
}
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const toAddresses = [this.calEvent.attendees[0].email];
|
const toAddresses = [this.calEvent.attendees[0].email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -30,7 +30,7 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
|
||||||
eventType: this.calEvent.type,
|
eventType: this.calEvent.type,
|
||||||
name: this.calEvent.attendees[0].name,
|
name: this.calEvent.attendees[0].name,
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("AttendeeWasRequestedToRescheduleEmail", {
|
html: await renderEmail("AttendeeWasRequestedToRescheduleEmail", {
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
attendee: this.calEvent.attendees[0],
|
attendee: this.calEvent.attendees[0],
|
||||||
metadata: this.metadata,
|
metadata: this.metadata,
|
||||||
|
|
|
@ -21,7 +21,7 @@ export default class BrokenIntegrationEmail extends BaseEmail {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const toAddresses = [this.calEvent.organizer.email];
|
const toAddresses = [this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -32,7 +32,7 @@ export default class BrokenIntegrationEmail extends BaseEmail {
|
||||||
name: this.calEvent.attendees[0].name,
|
name: this.calEvent.attendees[0].name,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("BrokenIntegrationEmail", {
|
html: await renderEmail("BrokenIntegrationEmail", {
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
attendee: this.calEvent.organizer,
|
attendee: this.calEvent.organizer,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { TFunction } from "next-i18next";
|
import type { TFunction } from "next-i18next";
|
||||||
|
|
||||||
import { renderEmail } from "..";
|
import { renderEmail } from "..";
|
||||||
import BaseEmail from "./_base-email";
|
import BaseEmail from "./_base-email";
|
||||||
|
@ -28,7 +28,7 @@ export default class DisabledAppEmail extends BaseEmail {
|
||||||
this.eventTypeId = eventTypeId;
|
this.eventTypeId = eventTypeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
from: `Cal.com <${this.getMailerOptions().from}>`,
|
from: `Cal.com <${this.getMailerOptions().from}>`,
|
||||||
to: this.email,
|
to: this.email,
|
||||||
|
@ -36,7 +36,7 @@ export default class DisabledAppEmail extends BaseEmail {
|
||||||
this.title && this.eventTypeId
|
this.title && this.eventTypeId
|
||||||
? this.t("disabled_app_affects_event_type", { appName: this.appName, eventType: this.title })
|
? this.t("disabled_app_affects_event_type", { appName: this.appName, eventType: this.title })
|
||||||
: this.t("admin_has_disabled", { appName: this.appName }),
|
: this.t("admin_has_disabled", { appName: this.appName }),
|
||||||
html: renderEmail("DisabledAppEmail", {
|
html: await renderEmail("DisabledAppEmail", {
|
||||||
title: this.title,
|
title: this.title,
|
||||||
appName: this.appName,
|
appName: this.appName,
|
||||||
eventTypeId: this.eventTypeId,
|
eventTypeId: this.eventTypeId,
|
||||||
|
|
|
@ -18,12 +18,12 @@ export default class FeedbackEmail extends BaseEmail {
|
||||||
this.feedback = feedback;
|
this.feedback = feedback;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: process.env.SEND_FEEDBACK_EMAIL,
|
to: process.env.SEND_FEEDBACK_EMAIL,
|
||||||
subject: `User Feedback`,
|
subject: `User Feedback`,
|
||||||
html: renderEmail("FeedbackEmail", this.feedback),
|
html: await renderEmail("FeedbackEmail", this.feedback),
|
||||||
text: this.getTextBody(),
|
text: this.getTextBody(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,14 +23,14 @@ export default class ForgotPasswordEmail extends BaseEmail {
|
||||||
this.passwordEvent = passwordEvent;
|
this.passwordEvent = passwordEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
to: `${this.passwordEvent.user.name} <${this.passwordEvent.user.email}>`,
|
to: `${this.passwordEvent.user.name} <${this.passwordEvent.user.email}>`,
|
||||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||||
subject: this.passwordEvent.language("reset_password_subject", {
|
subject: this.passwordEvent.language("reset_password_subject", {
|
||||||
appName: APP_NAME,
|
appName: APP_NAME,
|
||||||
}),
|
}),
|
||||||
html: renderEmail("ForgotPasswordEmail", this.passwordEvent),
|
html: await renderEmail("ForgotPasswordEmail", this.passwordEvent),
|
||||||
text: this.getTextBody(),
|
text: this.getTextBody(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,12 @@ export default class MonthlyDigestEmail extends BaseEmail {
|
||||||
this.eventData = eventData;
|
this.eventData = eventData;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: this.eventData.admin.email,
|
to: this.eventData.admin.email,
|
||||||
subject: `${APP_NAME}: Your monthly digest`,
|
subject: `${APP_NAME}: Your monthly digest`,
|
||||||
html: renderEmail("MonthlyDigestEmail", this.eventData),
|
html: await renderEmail("MonthlyDigestEmail", this.eventData),
|
||||||
text: "",
|
text: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { renderEmail } from "../";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
|
|
||||||
export default class NoShowFeeChargedEmail extends AttendeeScheduledEmail {
|
export default class NoShowFeeChargedEmail extends AttendeeScheduledEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
if (!this.calEvent.paymentInfo?.amount) throw new Error("No payment into");
|
if (!this.calEvent.paymentInfo?.amount) throw new Error("No payment into");
|
||||||
return {
|
return {
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
|
@ -14,7 +14,7 @@ export default class NoShowFeeChargedEmail extends AttendeeScheduledEmail {
|
||||||
amount: this.calEvent.paymentInfo.amount / 100,
|
amount: this.calEvent.paymentInfo.amount / 100,
|
||||||
formatParams: { amount: { currency: this.calEvent.paymentInfo?.currency } },
|
formatParams: { amount: { currency: this.calEvent.paymentInfo?.currency } },
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("NoShowFeeChargedEmail", {
|
html: await renderEmail("NoShowFeeChargedEmail", {
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
attendee: this.attendee,
|
attendee: this.attendee,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -22,7 +22,7 @@ export default class OrgAutoJoinEmail extends BaseEmail {
|
||||||
this.orgAutoInviteEvent = orgAutoInviteEvent;
|
this.orgAutoInviteEvent = orgAutoInviteEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
to: this.orgAutoInviteEvent.to,
|
to: this.orgAutoInviteEvent.to,
|
||||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||||
|
@ -32,7 +32,7 @@ export default class OrgAutoJoinEmail extends BaseEmail {
|
||||||
appName: APP_NAME,
|
appName: APP_NAME,
|
||||||
entity: this.orgAutoInviteEvent.language("organization").toLowerCase(),
|
entity: this.orgAutoInviteEvent.language("organization").toLowerCase(),
|
||||||
}),
|
}),
|
||||||
html: renderEmail("OrgAutoInviteEmail", this.orgAutoInviteEvent),
|
html: await renderEmail("OrgAutoInviteEmail", this.orgAutoInviteEvent),
|
||||||
text: "",
|
text: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,12 @@ export default class OrganizationEmailVerification extends BaseEmail {
|
||||||
this.orgVerifyInput = orgVerifyInput;
|
this.orgVerifyInput = orgVerifyInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: this.orgVerifyInput.user.email,
|
to: this.orgVerifyInput.user.email,
|
||||||
subject: this.orgVerifyInput.language("verify_email_organization"),
|
subject: this.orgVerifyInput.language("verify_email_organization"),
|
||||||
html: renderEmail("OrganisationAccountVerifyEmail", this.orgVerifyInput),
|
html: await renderEmail("OrganisationAccountVerifyEmail", this.orgVerifyInput),
|
||||||
text: this.getTextBody(),
|
text: this.getTextBody(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { renderEmail } from "../";
|
||||||
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||||
|
|
||||||
export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
|
export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const toAddresses = [this.calEvent.organizer.email];
|
const toAddresses = [this.calEvent.organizer.email];
|
||||||
if (this.calEvent.team) {
|
if (this.calEvent.team) {
|
||||||
this.calEvent.team.members.forEach((member) => {
|
this.calEvent.team.members.forEach((member) => {
|
||||||
|
@ -22,7 +22,7 @@ export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("OrganizerAttendeeCancelledSeatEmail", {
|
html: await renderEmail("OrganizerAttendeeCancelledSeatEmail", {
|
||||||
attendee: this.calEvent.organizer,
|
attendee: this.calEvent.organizer,
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { renderEmail } from "../";
|
||||||
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||||
|
|
||||||
export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
|
export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -14,7 +14,7 @@ export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("OrganizerCancelledEmail", {
|
html: await renderEmail("OrganizerCancelledEmail", {
|
||||||
attendee: this.calEvent.organizer,
|
attendee: this.calEvent.organizer,
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default class OrganizerDailyVideoDownloadRecordingEmail extends BaseEmail
|
||||||
this.downloadLink = downloadLink;
|
this.downloadLink = downloadLink;
|
||||||
this.t = this.calEvent.organizer.language.translate;
|
this.t = this.calEvent.organizer.language.translate;
|
||||||
}
|
}
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
to: `${this.calEvent.organizer.email}>`,
|
to: `${this.calEvent.organizer.email}>`,
|
||||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||||
|
@ -28,7 +28,7 @@ export default class OrganizerDailyVideoDownloadRecordingEmail extends BaseEmail
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("DailyVideoDownloadRecordingEmail", {
|
html: await renderEmail("DailyVideoDownloadRecordingEmail", {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
downloadLink: this.downloadLink,
|
downloadLink: this.downloadLink,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { renderEmail } from "../";
|
||||||
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||||
|
|
||||||
export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail {
|
export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -20,7 +20,7 @@ export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmai
|
||||||
name: this.calEvent.attendees[0].name,
|
name: this.calEvent.attendees[0].name,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("OrganizerLocationChangeEmail", {
|
html: await renderEmail("OrganizerLocationChangeEmail", {
|
||||||
attendee: this.calEvent.organizer,
|
attendee: this.calEvent.organizer,
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { renderEmail } from "../";
|
||||||
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||||
|
|
||||||
export default class OrganizerPaymentRefundFailedEmail extends OrganizerScheduledEmail {
|
export default class OrganizerPaymentRefundFailedEmail extends OrganizerScheduledEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -15,7 +15,7 @@ export default class OrganizerPaymentRefundFailedEmail extends OrganizerSchedule
|
||||||
name: this.calEvent.attendees[0].name,
|
name: this.calEvent.attendees[0].name,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("OrganizerPaymentRefundFailedEmail", {
|
html: await renderEmail("OrganizerPaymentRefundFailedEmail", {
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
attendee: this.calEvent.organizer,
|
attendee: this.calEvent.organizer,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { renderEmail } from "../";
|
||||||
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||||
|
|
||||||
export default class OrganizerRequestEmail extends OrganizerScheduledEmail {
|
export default class OrganizerRequestEmail extends OrganizerScheduledEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -12,7 +12,7 @@ export default class OrganizerRequestEmail extends OrganizerScheduledEmail {
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
|
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
|
||||||
subject: `${this.t("awaiting_approval")}: ${this.calEvent.title}`,
|
subject: `${this.t("awaiting_approval")}: ${this.calEvent.title}`,
|
||||||
html: renderEmail("OrganizerRequestEmail", {
|
html: await renderEmail("OrganizerRequestEmail", {
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
attendee: this.calEvent.organizer,
|
attendee: this.calEvent.organizer,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { renderEmail } from "../";
|
||||||
import OrganizerRequestEmail from "./organizer-request-email";
|
import OrganizerRequestEmail from "./organizer-request-email";
|
||||||
|
|
||||||
export default class OrganizerRequestReminderEmail extends OrganizerRequestEmail {
|
export default class OrganizerRequestReminderEmail extends OrganizerRequestEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -15,7 +15,7 @@ export default class OrganizerRequestReminderEmail extends OrganizerRequestEmail
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("OrganizerRequestReminderEmail", {
|
html: await renderEmail("OrganizerRequestReminderEmail", {
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
attendee: this.calEvent.organizer,
|
attendee: this.calEvent.organizer,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu
|
||||||
super({ calEvent });
|
super({ calEvent });
|
||||||
this.metadata = metadata;
|
this.metadata = metadata;
|
||||||
}
|
}
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const toAddresses = [this.calEvent.organizer.email];
|
const toAddresses = [this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -30,7 +30,7 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu
|
||||||
name: this.calEvent.attendees[0].name,
|
name: this.calEvent.attendees[0].name,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("OrganizerRequestedToRescheduleEmail", {
|
html: await renderEmail("OrganizerRequestedToRescheduleEmail", {
|
||||||
calEvent: this.calEvent,
|
calEvent: this.calEvent,
|
||||||
attendee: this.calEvent.organizer,
|
attendee: this.calEvent.organizer,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { renderEmail } from "../";
|
||||||
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||||
|
|
||||||
export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
|
export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -19,7 +19,7 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
date: this.getFormattedDate(),
|
date: this.getFormattedDate(),
|
||||||
})}`,
|
})}`,
|
||||||
html: renderEmail("OrganizerRescheduledEmail", {
|
html: await renderEmail("OrganizerRescheduledEmail", {
|
||||||
calEvent: { ...this.calEvent, attendeeSeatId: undefined },
|
calEvent: { ...this.calEvent, attendeeSeatId: undefined },
|
||||||
attendee: this.calEvent.organizer,
|
attendee: this.calEvent.organizer,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -70,7 +70,7 @@ export default class OrganizerScheduledEmail extends BaseEmail {
|
||||||
return icsEvent.value;
|
return icsEvent.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
const clonedCalEvent = cloneDeep(this.calEvent);
|
const clonedCalEvent = cloneDeep(this.calEvent);
|
||||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ export default class OrganizerScheduledEmail extends BaseEmail {
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
|
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
|
||||||
subject: `${this.newSeat ? `${this.t("new_attendee")}: ` : ""}${this.calEvent.title}`,
|
subject: `${this.newSeat ? `${this.t("new_attendee")}: ` : ""}${this.calEvent.title}`,
|
||||||
html: renderEmail("OrganizerScheduledEmail", {
|
html: await renderEmail("OrganizerScheduledEmail", {
|
||||||
calEvent: clonedCalEvent,
|
calEvent: clonedCalEvent,
|
||||||
attendee: this.calEvent.organizer,
|
attendee: this.calEvent.organizer,
|
||||||
teamMember: this.teamMember,
|
teamMember: this.teamMember,
|
||||||
|
|
|
@ -19,12 +19,12 @@ export default class SlugReplacementEmail extends BaseEmail {
|
||||||
this.t = t;
|
this.t = t;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
from: `Cal.com <${this.getMailerOptions().from}>`,
|
from: `Cal.com <${this.getMailerOptions().from}>`,
|
||||||
to: this.email,
|
to: this.email,
|
||||||
subject: this.t("email_subject_slug_replacement", { slug: this.slug }),
|
subject: this.t("email_subject_slug_replacement", { slug: this.slug }),
|
||||||
html: renderEmail("SlugReplacementEmail", {
|
html: await renderEmail("SlugReplacementEmail", {
|
||||||
slug: this.slug,
|
slug: this.slug,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
teamName: this.teamName || "",
|
teamName: this.teamName || "",
|
||||||
|
|
|
@ -24,7 +24,7 @@ export default class TeamInviteEmail extends BaseEmail {
|
||||||
this.teamInviteEvent = teamInviteEvent;
|
this.teamInviteEvent = teamInviteEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
to: this.teamInviteEvent.to,
|
to: this.teamInviteEvent.to,
|
||||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||||
|
@ -36,7 +36,7 @@ export default class TeamInviteEmail extends BaseEmail {
|
||||||
.language(this.teamInviteEvent.isOrg ? "organization" : "team")
|
.language(this.teamInviteEvent.isOrg ? "organization" : "team")
|
||||||
.toLowerCase(),
|
.toLowerCase(),
|
||||||
}),
|
}),
|
||||||
html: renderEmail("TeamInviteEmail", this.teamInviteEvent),
|
html: await renderEmail("TeamInviteEmail", this.teamInviteEvent),
|
||||||
text: "",
|
text: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ import {
|
||||||
expectBookingRequestedWebhookToHaveBeenFired,
|
expectBookingRequestedWebhookToHaveBeenFired,
|
||||||
expectSuccessfulCalendarEventDeletionInCalendar,
|
expectSuccessfulCalendarEventDeletionInCalendar,
|
||||||
expectSuccessfulVideoMeetingDeletionInCalendar,
|
expectSuccessfulVideoMeetingDeletionInCalendar,
|
||||||
expectSuccessfulRoudRobinReschedulingEmails,
|
expectSuccessfulRoundRobinReschedulingEmails,
|
||||||
} from "@calcom/web/test/utils/bookingScenario/expects";
|
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||||
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
|
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
|
||||||
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||||
|
@ -1692,7 +1692,7 @@ describe("handleNewBooking", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expectSuccessfulRoudRobinReschedulingEmails({
|
expectSuccessfulRoundRobinReschedulingEmails({
|
||||||
prevOrganizer: roundRobinHost1,
|
prevOrganizer: roundRobinHost1,
|
||||||
newOrganizer: roundRobinHost2,
|
newOrganizer: roundRobinHost2,
|
||||||
emails,
|
emails,
|
||||||
|
@ -1842,7 +1842,7 @@ describe("handleNewBooking", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expectSuccessfulRoudRobinReschedulingEmails({
|
expectSuccessfulRoundRobinReschedulingEmails({
|
||||||
prevOrganizer: roundRobinHost1,
|
prevOrganizer: roundRobinHost1,
|
||||||
newOrganizer: roundRobinHost1, // Round robin host 2 is not available and it will be rescheduled to same user
|
newOrganizer: roundRobinHost1, // Round robin host 2 is not available and it will be rescheduled to same user
|
||||||
emails,
|
emails,
|
||||||
|
|
|
@ -1,12 +1,24 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import type { ParsedUrlQuery } from "querystring";
|
import type { ParsedUrlQuery } from "querystring";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { queryNumberArray } from "@calcom/lib/hooks/useTypedQuery";
|
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
|
||||||
export type IEventTypesFilters = RouterOutputs["viewer"]["eventTypes"]["listWithTeam"];
|
export type IEventTypesFilters = RouterOutputs["viewer"]["eventTypes"]["listWithTeam"];
|
||||||
export type IEventTypeFilter = IEventTypesFilters[0];
|
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
|
// 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({
|
export const filterQuerySchema = z.object({
|
||||||
teamIds: queryNumberArray.optional(),
|
teamIds: queryNumberArray.optional(),
|
||||||
|
|
|
@ -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 (
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="hover:bg-subtle [&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis group-hover:text-default text-emphasis group my-6 flex h-6 max-h-6 w-full flex-row items-center rounded-md px-3 py-2 text-sm font-medium leading-4"
|
||||||
|
data-testid={`vertical-tab-${name}`}>
|
||||||
|
<ArrowLeft className="h-4 w-4 stroke-[2px] ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180 md:mt-0" />
|
||||||
|
<Skeleton title={name} as="p" className="max-w-36 min-h-4 truncate" loadingClassName="ms-3">
|
||||||
|
{name}
|
||||||
|
</Skeleton>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<nav
|
||||||
|
style={{ maxHeight: `calc(100vh - ${bannersHeight}px)`, top: `${bannersHeight}px` }}
|
||||||
|
className={classNames(
|
||||||
|
"no-scrollbar bg-muted fixed bottom-0 left-0 top-0 z-20 flex max-h-screen w-56 flex-col space-y-1 overflow-x-hidden overflow-y-scroll px-2 pb-3 transition-transform max-lg:z-10 lg:sticky lg:flex",
|
||||||
|
className,
|
||||||
|
navigationIsOpenedOnMobile
|
||||||
|
? "translate-x-0 opacity-100"
|
||||||
|
: "-translate-x-full opacity-0 lg:translate-x-0 lg:opacity-100"
|
||||||
|
)}
|
||||||
|
aria-label="Tabs">
|
||||||
|
<>
|
||||||
|
<BackButtonInSidebar name={t("back")} />
|
||||||
|
{tabsWithPermissions.map((tab) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={tab.href}>
|
||||||
|
{!["teams", "other_teams"].includes(tab.name) && (
|
||||||
|
<React.Fragment key={tab.href}>
|
||||||
|
<div className={`${!tab.children?.length ? "!mb-3" : ""}`}>
|
||||||
|
<div className="[&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis text-default group flex h-9 w-full flex-row items-center rounded-md px-2 text-sm font-medium leading-none">
|
||||||
|
{tab && tab.icon && (
|
||||||
|
<tab.icon className="h-[16px] w-[16px] stroke-[2px] ltr:mr-3 rtl:ml-3 md:mt-0" />
|
||||||
|
)}
|
||||||
|
{!tab.icon && tab?.avatar && (
|
||||||
|
<img
|
||||||
|
className="h-4 w-4 rounded-full ltr:mr-3 rtl:ml-3"
|
||||||
|
src={tab?.avatar}
|
||||||
|
alt="User Avatar"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Skeleton
|
||||||
|
title={tab.name}
|
||||||
|
as="p"
|
||||||
|
className="truncate text-sm font-medium leading-5"
|
||||||
|
loadingClassName="ms-3">
|
||||||
|
{t(tab.name)}
|
||||||
|
</Skeleton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="my-3 space-y-0.5">
|
||||||
|
{tab.children?.map((child, index) => (
|
||||||
|
<VerticalTabItem
|
||||||
|
key={child.href}
|
||||||
|
name={t(child.name)}
|
||||||
|
isExternalLink={child.isExternalLink}
|
||||||
|
href={child.href || "/"}
|
||||||
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
className={`my-0.5 me-5 h-7 ${
|
||||||
|
tab.children && index === tab.children?.length - 1 && "!mb-3"
|
||||||
|
}`}
|
||||||
|
disableChevron
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab.name === "teams" && (
|
||||||
|
<React.Fragment key={tab.href}>
|
||||||
|
<div className={`${!tab.children?.length ? "mb-3" : ""}`}>
|
||||||
|
<Link href={tab.href}>
|
||||||
|
<div className="hover:bg-subtle [&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis group-hover:text-default text-default group flex h-9 w-full flex-row items-center rounded-md px-2 py-[10px] text-sm font-medium leading-none">
|
||||||
|
{tab && tab.icon && (
|
||||||
|
<tab.icon className="h-[16px] w-[16px] stroke-[2px] ltr:mr-3 rtl:ml-3 md:mt-0" />
|
||||||
|
)}
|
||||||
|
<Skeleton
|
||||||
|
title={tab.name}
|
||||||
|
as="p"
|
||||||
|
className="truncate text-sm font-medium leading-5"
|
||||||
|
loadingClassName="ms-3">
|
||||||
|
{t(isOrgAdminOrOwner ? "my_teams" : tab.name)}
|
||||||
|
</Skeleton>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
{teams &&
|
||||||
|
teamMenuState &&
|
||||||
|
teams.map((team, index: number) => {
|
||||||
|
if (!teamMenuState[index]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (teamMenuState.some((teamState) => teamState.teamId === team.id))
|
||||||
|
return (
|
||||||
|
<Collapsible
|
||||||
|
className="cursor-pointer"
|
||||||
|
key={team.id}
|
||||||
|
open={teamMenuState[index].teamMenuOpen}
|
||||||
|
onOpenChange={() =>
|
||||||
|
setTeamMenuState([
|
||||||
|
...teamMenuState,
|
||||||
|
(teamMenuState[index] = {
|
||||||
|
...teamMenuState[index],
|
||||||
|
teamMenuOpen: !teamMenuState[index].teamMenuOpen,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<div
|
||||||
|
className="hover:bg-subtle [&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis text-default flex h-9 w-full flex-row items-center rounded-md px-3 py-[10px] text-left text-sm font-medium leading-none"
|
||||||
|
onClick={() =>
|
||||||
|
setTeamMenuState([
|
||||||
|
...teamMenuState,
|
||||||
|
(teamMenuState[index] = {
|
||||||
|
...teamMenuState[index],
|
||||||
|
teamMenuOpen: !teamMenuState[index].teamMenuOpen,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}>
|
||||||
|
<div className="me-3">
|
||||||
|
{teamMenuState[index].teamMenuOpen ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!team.parentId && (
|
||||||
|
<img
|
||||||
|
src={getPlaceholderAvatar(team.logo, team?.name as string)}
|
||||||
|
className="h-[16px] w-[16px] self-start rounded-full stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0"
|
||||||
|
alt={team.name || "Team logo"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<p className="w-1/2 truncate leading-normal">{team.name}</p>
|
||||||
|
{!team.accepted && (
|
||||||
|
<Badge className="ms-3" variant="orange">
|
||||||
|
Inv.
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent className="space-y-0.5">
|
||||||
|
{team.accepted && (
|
||||||
|
<VerticalTabItem
|
||||||
|
name={t("profile")}
|
||||||
|
href={`/settings/teams/${team.id}/profile`}
|
||||||
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
disableChevron
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<VerticalTabItem
|
||||||
|
name={t("members")}
|
||||||
|
href={`/settings/teams/${team.id}/members`}
|
||||||
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
disableChevron
|
||||||
|
/>
|
||||||
|
{(team.role === MembershipRole.OWNER ||
|
||||||
|
team.role === MembershipRole.ADMIN ||
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore this exists wtf?
|
||||||
|
(team.isOrgAdmin && team.isOrgAdmin)) && (
|
||||||
|
<>
|
||||||
|
{/* TODO */}
|
||||||
|
{/* <VerticalTabItem
|
||||||
|
name={t("general")}
|
||||||
|
href={`${WEBAPP_URL}/settings/my-account/appearance`}
|
||||||
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
disableChevron
|
||||||
|
/> */}
|
||||||
|
<VerticalTabItem
|
||||||
|
name={t("appearance")}
|
||||||
|
href={`/settings/teams/${team.id}/appearance`}
|
||||||
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
disableChevron
|
||||||
|
/>
|
||||||
|
{/* Hide if there is a parent ID */}
|
||||||
|
{!team.parentId ? (
|
||||||
|
<>
|
||||||
|
<VerticalTabItem
|
||||||
|
name={t("billing")}
|
||||||
|
href={`/settings/teams/${team.id}/billing`}
|
||||||
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
disableChevron
|
||||||
|
/>
|
||||||
|
{HOSTED_CAL_FEATURES && (
|
||||||
|
<VerticalTabItem
|
||||||
|
name={t("saml_config")}
|
||||||
|
href={`/settings/teams/${team.id}/sso`}
|
||||||
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
disableChevron
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{(!currentOrg || (currentOrg && currentOrg?.user?.role !== "MEMBER")) && (
|
||||||
|
<VerticalTabItem
|
||||||
|
name={t("add_a_team")}
|
||||||
|
href={`${WEBAPP_URL}/settings/teams/new`}
|
||||||
|
textClassNames="px-3 items-center mt-2 text-emphasis font-medium text-sm"
|
||||||
|
icon={Plus}
|
||||||
|
disableChevron
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab.name === "other_teams" && (
|
||||||
|
<React.Fragment key={tab.href}>
|
||||||
|
<div className={`${!tab.children?.length ? "mb-3" : ""}`}>
|
||||||
|
<Link href={tab.href}>
|
||||||
|
<div className="hover:bg-subtle [&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis group-hover:text-default text-default group flex h-9 w-full flex-row items-center rounded-md px-2 py-[10px] text-sm font-medium leading-none">
|
||||||
|
{tab && tab.icon && (
|
||||||
|
<tab.icon className="h-[16px] w-[16px] stroke-[2px] ltr:mr-3 rtl:ml-3 md:mt-0" />
|
||||||
|
)}
|
||||||
|
<Skeleton
|
||||||
|
title={t("org_admin_other_teams")}
|
||||||
|
as="p"
|
||||||
|
className="truncate text-sm font-medium leading-5"
|
||||||
|
loadingClassName="ms-3">
|
||||||
|
{t("org_admin_other_teams")}
|
||||||
|
</Skeleton>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
{otherTeams &&
|
||||||
|
otherTeamMenuState &&
|
||||||
|
otherTeams.map((otherTeam, index: number) => {
|
||||||
|
if (!otherTeamMenuState[index]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (otherTeamMenuState.some((teamState) => teamState.teamId === otherTeam.id))
|
||||||
|
return (
|
||||||
|
<Collapsible
|
||||||
|
className="cursor-pointer"
|
||||||
|
key={otherTeam.id}
|
||||||
|
open={otherTeamMenuState[index].teamMenuOpen}
|
||||||
|
onOpenChange={() =>
|
||||||
|
setOtherTeamMenuState([
|
||||||
|
...otherTeamMenuState,
|
||||||
|
(otherTeamMenuState[index] = {
|
||||||
|
...otherTeamMenuState[index],
|
||||||
|
teamMenuOpen: !otherTeamMenuState[index].teamMenuOpen,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<div
|
||||||
|
className="hover:bg-subtle [&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis text-default flex h-9 w-full flex-row items-center rounded-md px-3 py-[10px] text-left text-sm font-medium leading-none"
|
||||||
|
onClick={() =>
|
||||||
|
setOtherTeamMenuState([
|
||||||
|
...otherTeamMenuState,
|
||||||
|
(otherTeamMenuState[index] = {
|
||||||
|
...otherTeamMenuState[index],
|
||||||
|
teamMenuOpen: !otherTeamMenuState[index].teamMenuOpen,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}>
|
||||||
|
<div className="me-3">
|
||||||
|
{otherTeamMenuState[index].teamMenuOpen ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!otherTeam.parentId && (
|
||||||
|
<img
|
||||||
|
src={getPlaceholderAvatar(otherTeam.logo, otherTeam?.name as string)}
|
||||||
|
className="h-[16px] w-[16px] self-start rounded-full stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0"
|
||||||
|
alt={otherTeam.name || "Team logo"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<p className="w-1/2 truncate leading-normal">{otherTeam.name}</p>
|
||||||
|
</div>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent className="space-y-0.5">
|
||||||
|
<VerticalTabItem
|
||||||
|
name={t("profile")}
|
||||||
|
href={`/settings/organizations/teams/other/${otherTeam.id}/profile`}
|
||||||
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
disableChevron
|
||||||
|
/>
|
||||||
|
<VerticalTabItem
|
||||||
|
name={t("members")}
|
||||||
|
href={`/settings/organizations/teams/other/${otherTeam.id}/members`}
|
||||||
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
disableChevron
|
||||||
|
/>
|
||||||
|
|
||||||
|
<>
|
||||||
|
{/* TODO: enable appearance edit */}
|
||||||
|
{/* <VerticalTabItem
|
||||||
|
name={t("appearance")}
|
||||||
|
href={`/settings/organizations/teams/other/${otherTeam.id}/appearance`}
|
||||||
|
textClassNames="px-3 text-emphasis font-medium text-sm"
|
||||||
|
disableChevron
|
||||||
|
/> */}
|
||||||
|
</>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<nav className="bg-muted border-muted sticky top-0 z-20 flex w-full items-center justify-between border-b py-2 sm:relative lg:hidden">
|
||||||
|
<div className="flex items-center space-x-3 ">
|
||||||
|
<Button StartIcon={Menu} color="minimal" variant="icon" onClick={props.onSideContainerOpen}>
|
||||||
|
<span className="sr-only">{t("show_navigation")}</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="hover:bg-emphasis flex items-center space-x-2 rounded-md px-3 py-1 rtl:space-x-reverse"
|
||||||
|
onClick={() => router.back()}>
|
||||||
|
<ArrowLeft className="text-default h-4 w-4" />
|
||||||
|
<p className="text-emphasis font-semibold">{t("settings")}</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SettingsLayout({
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||||
|
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 (
|
||||||
|
<Shell
|
||||||
|
withoutSeo={true}
|
||||||
|
flexChildrenContainer
|
||||||
|
hideHeadingOnMobile
|
||||||
|
{...rest}
|
||||||
|
SidebarContainer={
|
||||||
|
<SidebarContainerElement
|
||||||
|
sideContainerOpen={sideContainerOpen}
|
||||||
|
setSideContainerOpen={setSideContainerOpen}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
drawerState={state}
|
||||||
|
MobileNavigationContainer={null}
|
||||||
|
TopNavContainer={
|
||||||
|
<MobileSettingsContainer onSideContainerOpen={() => setSideContainerOpen(!sideContainerOpen)} />
|
||||||
|
}>
|
||||||
|
<div className="flex flex-1 [&>*]:flex-1">
|
||||||
|
<div className="mx-auto max-w-full justify-center lg:max-w-4xl">
|
||||||
|
<ShellHeader />
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Suspense fallback={<Loader />}>{children}</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Shell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarContainerElement = ({
|
||||||
|
sideContainerOpen,
|
||||||
|
bannersHeight,
|
||||||
|
setSideContainerOpen,
|
||||||
|
}: SidebarContainerElementProps) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile backdrop */}
|
||||||
|
{sideContainerOpen && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSideContainerOpen(false)}
|
||||||
|
className="fixed left-0 top-0 z-10 h-full w-full bg-black/50">
|
||||||
|
<span className="sr-only">{t("hide_navigation")}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<SettingsSidebarContainer
|
||||||
|
navigationIsOpenedOnMobile={sideContainerOpen}
|
||||||
|
bannersHeight={bannersHeight}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type SidebarContainerElementProps = {
|
||||||
|
sideContainerOpen: boolean;
|
||||||
|
bannersHeight?: number;
|
||||||
|
setSideContainerOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLayout = (page: React.ReactElement) => <SettingsLayout>{page}</SettingsLayout>;
|
||||||
|
|
||||||
|
export function ShellHeader() {
|
||||||
|
const { meta } = useMeta();
|
||||||
|
const { t, isLocaleReady } = useLocale();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header
|
||||||
|
className={classNames(
|
||||||
|
"border-subtle mx-auto block justify-between sm:flex",
|
||||||
|
meta.borderInShellHeader && "rounded-t-lg border px-4 py-6 sm:px-6",
|
||||||
|
meta.borderInShellHeader === undefined && "mb-8 border-b pb-8"
|
||||||
|
)}>
|
||||||
|
<div className="flex w-full items-center">
|
||||||
|
{meta.backButton && (
|
||||||
|
<a href="javascript:history.back()">
|
||||||
|
<ArrowLeft className="mr-7" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
{meta.title && isLocaleReady ? (
|
||||||
|
<h1 className="font-cal text-emphasis mb-1 text-xl font-bold leading-5 tracking-wide">
|
||||||
|
{t(meta.title)}
|
||||||
|
</h1>
|
||||||
|
) : (
|
||||||
|
<div className="bg-emphasis mb-1 h-5 w-24 animate-pulse rounded-lg" />
|
||||||
|
)}
|
||||||
|
{meta.description && isLocaleReady ? (
|
||||||
|
<p className="text-default text-sm ltr:mr-4 rtl:ml-4">{t(meta.description)}</p>
|
||||||
|
) : (
|
||||||
|
<div className="bg-emphasis h-5 w-32 animate-pulse rounded-lg" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="ms-auto flex-shrink-0">{meta.CTA}</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useCallback, useMemo, useEffect } from "react";
|
import { useCallback, useMemo, useEffect } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
|
@ -1 +1,29 @@
|
||||||
export * from "@trpc/react-query/shared";
|
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;
|
||||||
|
|
|
@ -11,38 +11,13 @@ import { createTRPCNext } from "../next";
|
||||||
import type { TRPCClientErrorLike } from "../react";
|
import type { TRPCClientErrorLike } from "../react";
|
||||||
import type { inferRouterInputs, inferRouterOutputs, Maybe } from "../server";
|
import type { inferRouterInputs, inferRouterOutputs, Maybe } from "../server";
|
||||||
import type { AppRouter } from "../server/routers/_app";
|
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
|
* 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?
|
* 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];
|
export type Endpoint = (typeof ENDPOINTS)[number];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { filterQuerySchemaStrict } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
import { filterQuerySchemaStrict } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||||
|
|
|
@ -199,6 +199,7 @@
|
||||||
"ANALYZE",
|
"ANALYZE",
|
||||||
"API_KEY_PREFIX",
|
"API_KEY_PREFIX",
|
||||||
"APP_ROUTER_EVENT_TYPES_ENABLED",
|
"APP_ROUTER_EVENT_TYPES_ENABLED",
|
||||||
|
"APP_ROUTER_SETTINGS_ADMIN_ENABLED",
|
||||||
"APP_USER_NAME",
|
"APP_USER_NAME",
|
||||||
"BASECAMP3_CLIENT_ID",
|
"BASECAMP3_CLIENT_ID",
|
||||||
"BASECAMP3_CLIENT_SECRET",
|
"BASECAMP3_CLIENT_SECRET",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user