From 706d7dff42f57ec1584d3b16a4cb46f52ca3ce10 Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Mon, 9 Sep 2024 17:38:51 -0400 Subject: [PATCH] perf: Server-Side Data Fetching in App Router: `SettingsLayout` / `AdminLayout` (#16537) Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> --- apps/web/app/future/auth/logout/page.tsx | 12 +- .../future/settings/(admin)/admin/page.tsx | 2 +- .../app/future/settings/(admin)/layout.tsx | 2 +- .../app/future/settings/(settings)/layout.tsx | 2 +- .../[id]/onboard-admins/page.tsx | 19 +- .../organizations/appearance/layout.tsx | 2 +- .../organizations/billing/layout.tsx | 2 +- .../organizations/general/layout.tsx | 2 +- .../organizations/members/layout.tsx | 2 +- .../organizations/profile/layout.tsx | 2 +- .../teams/other/[id]/appearance/layout.tsx | 2 +- .../teams/other/[id]/members/layout.tsx | 2 +- .../teams/other/[id]/profile/layout.tsx | 2 +- .../organizations/teams/other/layout.tsx | 2 +- .../settings/(settings)/teams/layout.tsx | 2 +- apps/web/app/layoutHOC.tsx | 19 +- apps/web/components/PageWrapperAppDir.tsx | 11 +- .../auth/layouts/AdminLayoutAppDir.tsx | 49 +- .../auth/layouts/AdminLayoutAppDirClient.tsx | 34 + packages/features/MainLayoutAppDir.tsx | 8 +- .../settings/appDir/SettingsLayoutAppDir.tsx | 750 +----------------- .../appDir/SettingsLayoutAppDirClient.tsx | 745 +++++++++++++++++ .../lib/server/repository/organization.ts | 61 +- .../viewer/organizations/list.handler.ts | 52 +- .../organizations/listOtherTeams.handler.ts | 17 +- 25 files changed, 929 insertions(+), 874 deletions(-) create mode 100644 apps/web/components/auth/layouts/AdminLayoutAppDirClient.tsx create mode 100644 packages/features/settings/appDir/SettingsLayoutAppDirClient.tsx diff --git a/apps/web/app/future/auth/logout/page.tsx b/apps/web/app/future/auth/logout/page.tsx index 0c7a19efd5..f204257d5d 100644 --- a/apps/web/app/future/auth/logout/page.tsx +++ b/apps/web/app/future/auth/logout/page.tsx @@ -1,11 +1,10 @@ import type { PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; +import { WithLayout } from "app/layoutHOC"; import { cookies, headers } from "next/headers"; import { buildLegacyCtx } from "@lib/buildLegacyCtx"; -import PageWrapper from "@components/PageWrapperAppDir"; - import { ssrInit } from "@server/lib/ssr"; import Logout from "~/auth/logout-view"; @@ -20,7 +19,6 @@ export const generateMetadata = async () => { const Page = async ({ params, searchParams }: PageProps) => { // cookie will be cleared in `/apps/web/middleware.ts` const h = headers(); - const nonce = h.get("x-nonce") ?? undefined; const context = buildLegacyCtx(h, cookies(), params, searchParams); const ssr = await ssrInit(context); const props = { @@ -28,11 +26,7 @@ const Page = async ({ params, searchParams }: PageProps) => { query: context.query, }; - return ( - - - - ); + return ; }; -export default Page; +export default WithLayout({ getLayout: null, ServerPage: Page })<"P">; diff --git a/apps/web/app/future/settings/(admin)/admin/page.tsx b/apps/web/app/future/settings/(admin)/admin/page.tsx index cfc6e0aeec..77f09f99a3 100644 --- a/apps/web/app/future/settings/(admin)/admin/page.tsx +++ b/apps/web/app/future/settings/(admin)/admin/page.tsx @@ -10,4 +10,4 @@ export const generateMetadata = async () => () => "admin_description" ); -export default WithLayout({ getLayout, Page: LegacyPage })<"P">; +export default WithLayout({ getServerLayout: getLayout, Page: LegacyPage })<"P">; diff --git a/apps/web/app/future/settings/(admin)/layout.tsx b/apps/web/app/future/settings/(admin)/layout.tsx index f415bae764..4a29db5abe 100644 --- a/apps/web/app/future/settings/(admin)/layout.tsx +++ b/apps/web/app/future/settings/(admin)/layout.tsx @@ -2,4 +2,4 @@ import { WithLayout } from "app/layoutHOC"; import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir"; -export default WithLayout({ getLayout })<"L">; +export default WithLayout({ getServerLayout: getLayout })<"L">; diff --git a/apps/web/app/future/settings/(settings)/layout.tsx b/apps/web/app/future/settings/(settings)/layout.tsx index 3fc739975d..1e4a657288 100644 --- a/apps/web/app/future/settings/(settings)/layout.tsx +++ b/apps/web/app/future/settings/(settings)/layout.tsx @@ -2,4 +2,4 @@ import { WithLayout } from "app/layoutHOC"; import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; -export default WithLayout({ getLayout: getLayout })<"L">; +export default WithLayout({ getServerLayout: getLayout })<"L">; diff --git a/apps/web/app/future/settings/(settings)/organizations/[id]/onboard-admins/page.tsx b/apps/web/app/future/settings/(settings)/organizations/[id]/onboard-admins/page.tsx index 3bac3fb49d..1db37f76f5 100644 --- a/apps/web/app/future/settings/(settings)/organizations/[id]/onboard-admins/page.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/[id]/onboard-admins/page.tsx @@ -3,9 +3,7 @@ import LegacyPage, { } from "@pages/settings/organizations/[id]/onboard-members"; import type { PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; -import { headers } from "next/headers"; - -import PageWrapper from "@components/PageWrapperAppDir"; +import { WithLayout } from "app/layoutHOC"; export const generateMetadata = async () => await _generateMetadata( @@ -14,18 +12,7 @@ export const generateMetadata = async () => ); const Page = ({ params }: PageProps) => { - const h = headers(); - const nonce = h.get("x-nonce") ?? undefined; - - return ( - buildWrappedOnboardTeamMembersPage(params.id, page)} - requiresLicense={false} - nonce={nonce} - themeBasis={null}> - - - ); + return buildWrappedOnboardTeamMembersPage(params.id, ); }; -export default Page; +export default WithLayout({ Page }); diff --git a/apps/web/app/future/settings/(settings)/organizations/appearance/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/appearance/layout.tsx index 517645e71f..9da17cb281 100644 --- a/apps/web/app/future/settings/(settings)/organizations/appearance/layout.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/appearance/layout.tsx @@ -2,4 +2,4 @@ import { WithLayout } from "app/layoutHOC"; import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; -export default WithLayout({ getLayout }); +export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/billing/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/billing/layout.tsx index 517645e71f..9da17cb281 100644 --- a/apps/web/app/future/settings/(settings)/organizations/billing/layout.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/billing/layout.tsx @@ -2,4 +2,4 @@ import { WithLayout } from "app/layoutHOC"; import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; -export default WithLayout({ getLayout }); +export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/general/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/general/layout.tsx index 517645e71f..9da17cb281 100644 --- a/apps/web/app/future/settings/(settings)/organizations/general/layout.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/general/layout.tsx @@ -2,4 +2,4 @@ import { WithLayout } from "app/layoutHOC"; import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; -export default WithLayout({ getLayout }); +export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/members/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/members/layout.tsx index 517645e71f..9da17cb281 100644 --- a/apps/web/app/future/settings/(settings)/organizations/members/layout.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/members/layout.tsx @@ -2,4 +2,4 @@ import { WithLayout } from "app/layoutHOC"; import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; -export default WithLayout({ getLayout }); +export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/profile/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/profile/layout.tsx index 517645e71f..9da17cb281 100644 --- a/apps/web/app/future/settings/(settings)/organizations/profile/layout.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/profile/layout.tsx @@ -2,4 +2,4 @@ import { WithLayout } from "app/layoutHOC"; import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; -export default WithLayout({ getLayout }); +export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/layout.tsx index 517645e71f..9da17cb281 100644 --- a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/layout.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/layout.tsx @@ -2,4 +2,4 @@ import { WithLayout } from "app/layoutHOC"; import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; -export default WithLayout({ getLayout }); +export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/layout.tsx index 517645e71f..9da17cb281 100644 --- a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/layout.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/layout.tsx @@ -2,4 +2,4 @@ import { WithLayout } from "app/layoutHOC"; import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; -export default WithLayout({ getLayout }); +export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/layout.tsx index 517645e71f..9da17cb281 100644 --- a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/layout.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/layout.tsx @@ -2,4 +2,4 @@ import { WithLayout } from "app/layoutHOC"; import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; -export default WithLayout({ getLayout }); +export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/layout.tsx index 517645e71f..9da17cb281 100644 --- a/apps/web/app/future/settings/(settings)/organizations/teams/other/layout.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/teams/other/layout.tsx @@ -2,4 +2,4 @@ import { WithLayout } from "app/layoutHOC"; import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; -export default WithLayout({ getLayout }); +export default WithLayout({ getServerLayout: getLayout }); diff --git a/apps/web/app/future/settings/(settings)/teams/layout.tsx b/apps/web/app/future/settings/(settings)/teams/layout.tsx index 31b58d652c..1e4a657288 100644 --- a/apps/web/app/future/settings/(settings)/teams/layout.tsx +++ b/apps/web/app/future/settings/(settings)/teams/layout.tsx @@ -2,4 +2,4 @@ import { WithLayout } from "app/layoutHOC"; import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; -export default WithLayout({ getLayout })<"L">; +export default WithLayout({ getServerLayout: getLayout })<"L">; diff --git a/apps/web/app/layoutHOC.tsx b/apps/web/app/layoutHOC.tsx index 9ea33df3fd..cd7070b270 100644 --- a/apps/web/app/layoutHOC.tsx +++ b/apps/web/app/layoutHOC.tsx @@ -7,8 +7,10 @@ import { buildLegacyCtx } from "@lib/buildLegacyCtx"; import PageWrapper from "@components/PageWrapperAppDir"; type WithLayoutParams> = { - getLayout: ((page: React.ReactElement) => React.ReactNode) | null; + getLayout?: ((page: React.ReactElement) => React.ReactNode) | null; + getServerLayout?: (page: React.ReactElement) => Promise; Page?: (props: T) => React.ReactElement | null; + ServerPage?: (props: T) => Promise | null; getData?: (arg: GetServerSidePropsContext) => Promise; isBookingPage?: boolean; requiresLicense?: boolean; @@ -16,7 +18,9 @@ type WithLayoutParams> = { export function WithLayout>({ getLayout, + getServerLayout, getData, + ServerPage, Page, isBookingPage, requiresLicense, @@ -30,7 +34,16 @@ export function WithLayout>({ props = (await getData(buildLegacyCtx(h, cookies(), p.params, p.searchParams))) ?? ({} as T); } - const children = "children" in p ? p.children : null; + // `p.children` exists only for layout.tsx files + const childrenFromLayoutFile = "children" in p ? p.children : null; + const page = ServerPage ? ( + await ServerPage({ ...props, ...p }) + ) : Page ? ( + + ) : ( + childrenFromLayoutFile + ); + const pageWithServerLayout = page ? (getServerLayout ? await getServerLayout(page) : page) : null; return ( >({ themeBasis={null} isBookingPage={isBookingPage || !!(Page && "isBookingPage" in Page && Page.isBookingPage)} {...props}> - {Page ? : children} + {pageWithServerLayout} ); }; diff --git a/apps/web/components/PageWrapperAppDir.tsx b/apps/web/components/PageWrapperAppDir.tsx index ae36417c97..e729918bd4 100644 --- a/apps/web/components/PageWrapperAppDir.tsx +++ b/apps/web/components/PageWrapperAppDir.tsx @@ -5,21 +5,14 @@ import type { SSRConfig } from "next-i18next"; // import I18nLanguageHandler from "@components/I18nLanguageHandler"; import { usePathname } from "next/navigation"; import Script from "next/script"; -import type { ReactNode } from "react"; import "@calcom/embed-core/src/embed-iframe"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; -import type { AppProps } from "@lib/app-providers-app-dir"; import AppProviders from "@lib/app-providers-app-dir"; -export interface CalPageWrapper { - (props?: AppProps): JSX.Element; - PageWrapper?: AppProps["Component"]["PageWrapper"]; -} - export type PageWrapperProps = Readonly<{ - getLayout: ((page: React.ReactElement) => ReactNode) | null; + getLayout?: ((page: React.ReactElement) => React.ReactNode) | null; children: React.ReactNode; requiresLicense: boolean; nonce: string | undefined; @@ -50,7 +43,7 @@ function PageWrapper(props: PageWrapperProps) { nonce, }; - const getLayout: (page: React.ReactElement) => ReactNode = props.getLayout ?? ((page) => page); + const getLayout: (page: React.ReactElement) => React.ReactNode = props.getLayout ?? ((page) => page); return ( diff --git a/apps/web/components/auth/layouts/AdminLayoutAppDir.tsx b/apps/web/components/auth/layouts/AdminLayoutAppDir.tsx index e07c877b68..8404786384 100644 --- a/apps/web/components/auth/layouts/AdminLayoutAppDir.tsx +++ b/apps/web/components/auth/layouts/AdminLayoutAppDir.tsx @@ -1,40 +1,23 @@ -"use client"; +import { getServerSession } from "next-auth"; +import dynamic from "next/dynamic"; +import React from "react"; -import { useSession } from "next-auth/react"; -import { usePathname, useRouter } from "next/navigation"; -import type { ComponentProps } from "react"; -import React, { useEffect } from "react"; +import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options"; +import SettingsLayoutAppDir from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; -import SettingsLayout from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; -import type Shell from "@calcom/features/shell/Shell"; -import { UserPermissionRole } from "@calcom/prisma/enums"; -import { ErrorBoundary } from "@calcom/ui"; +import type { AdminLayoutProps } from "./AdminLayoutAppDirClient"; -export default function AdminLayout({ - children, - ...rest -}: { children: React.ReactNode } & ComponentProps) { - const pathname = usePathname(); - const session = useSession(); - const router = useRouter(); +const AdminLayoutAppDirClient = dynamic(() => import("./AdminLayoutAppDirClient"), { + ssr: false, +}); - // Force redirect on component level - useEffect(() => { - if (session.data && session.data.user.role !== UserPermissionRole.ADMIN) { - router.replace("/settings/my-account/profile"); - } - }, [session, router]); +type AdminLayoutAppDirProps = Omit; - const isAppsPage = pathname?.startsWith("/settings/admin/apps"); - return ( - -
-
*]:flex-1"}> - {children} -
-
-
- ); +export default async function AdminLayoutAppDir(props: AdminLayoutAppDirProps) { + const session = await getServerSession(AUTH_OPTIONS); + const userRole = session?.user?.role; + + return await SettingsLayoutAppDir({ children: }); } -export const getLayout = (page: React.ReactElement) => {page}; +export const getLayout = async (page: React.ReactElement) => await AdminLayoutAppDir({ children: page }); diff --git a/apps/web/components/auth/layouts/AdminLayoutAppDirClient.tsx b/apps/web/components/auth/layouts/AdminLayoutAppDirClient.tsx new file mode 100644 index 0000000000..380604fee8 --- /dev/null +++ b/apps/web/components/auth/layouts/AdminLayoutAppDirClient.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; +import type { ComponentProps } from "react"; +import React, { useEffect } from "react"; + +import type Shell from "@calcom/features/shell/Shell"; +import { UserPermissionRole } from "@calcom/prisma/enums"; +import { ErrorBoundary } from "@calcom/ui"; + +export type AdminLayoutProps = { + children: React.ReactNode; + userRole: UserPermissionRole | "INACTIVE_ADMIN" | undefined; +} & ComponentProps; +export default function AdminLayoutAppDirClient({ userRole, children }: AdminLayoutProps) { + const pathname = usePathname(); + const router = useRouter(); + + // Force redirect on component level + useEffect(() => { + if (userRole !== UserPermissionRole.ADMIN) { + router.replace("/settings/my-account/profile"); + } + }, [userRole, router]); + + const isAppsPage = pathname?.startsWith("/settings/admin/apps"); + return ( +
+
*]:flex-1"}> + {children} +
+
+ ); +} diff --git a/packages/features/MainLayoutAppDir.tsx b/packages/features/MainLayoutAppDir.tsx index acf4a2b522..6d14c05f74 100644 --- a/packages/features/MainLayoutAppDir.tsx +++ b/packages/features/MainLayoutAppDir.tsx @@ -5,10 +5,8 @@ import React from "react"; import Shell from "@calcom/features/shell/Shell"; -export default function MainLayout({ - children, - ...rest -}: { children: React.ReactNode } & ComponentProps) { +export type MainLayoutAppDirProps = { children: React.ReactNode } & ComponentProps; +export default function MainLayoutAppDir({ children, ...rest }: MainLayoutAppDirProps) { return ( {children} @@ -16,4 +14,4 @@ export default function MainLayout({ ); } -export const getLayout = (page: React.ReactElement) => {page}; +export const getLayout = (page: React.ReactElement) => {page}; diff --git a/packages/features/settings/appDir/SettingsLayoutAppDir.tsx b/packages/features/settings/appDir/SettingsLayoutAppDir.tsx index d96b7f7972..fb2936b4b5 100644 --- a/packages/features/settings/appDir/SettingsLayoutAppDir.tsx +++ b/packages/features/settings/appDir/SettingsLayoutAppDir.tsx @@ -1,737 +1,37 @@ -"use client"; +import { getServerSession } from "next-auth"; +import dynamic from "next/dynamic"; +import React from "react"; -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, { useEffect, useState, useMemo } from "react"; +import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options"; +import { OrganizationRepository } from "@calcom/lib/server/repository/organization"; -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, Icon, Skeleton, VerticalTabItem } from "@calcom/ui"; +import type { SettingsLayoutProps } from "./SettingsLayoutAppDirClient"; -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" }, - { name: "out_of_office", href: "/settings/my-account/out-of-office" }, - // 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: "credit-card", - 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" }, - { name: "admin_api", href: "/settings/organizations/admin-api" }, - // 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: "privacy", - href: "/settings/organizations/privacy", - }, - { - name: "billing", - href: "/settings/organizations/billing", - }, - { name: "OAuth Clients", href: "/settings/organizations/platform/oauth-clients" }, - { - name: "SSO", - href: "/settings/organizations/sso", - }, - { - name: "directory_sync", - href: "/settings/organizations/dsync", - }, - { - name: "admin_api", - href: "https://cal.com/docs/enterprise-features/api/api-reference/bookings#admin-access", - }, - ], - }, - { - name: "teams", - href: "/teams", - icon: "users", - children: [], - }, - { - name: "other_teams", - href: "/settings/organizations/teams/other", - 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: "lockedSMS", href: "/settings/admin/lockedSMS" }, - { name: "oAuth", href: "/settings/admin/oAuth" }, - ], - }, -]; - -tabs.find((tab) => { - if (tab.name === "security" && !HOSTED_CAL_FEATURES) { - tab.children?.push({ name: "sso_configuration", href: "/settings/security/sso" }); - // TODO: Enable dsync for self hosters - // tab.children?.push({ name: "directory_sync", href: "/settings/security/dsync" }); - } +const SettingsLayoutAppDirClient = dynamic(() => import("./SettingsLayoutAppDirClient"), { + ssr: false, }); -// The following keys are assigned to admin only -const adminRequiredKeys = ["admin"]; -const organizationRequiredKeys = ["organization"]; -const organizationAdminKeys = ["privacy", "billing", "OAuth Clients", "SSO", "directory_sync"]; +type SettingsLayoutAppDir = Omit; -const useTabs = () => { - const session = useSession(); - const { data: user } = trpc.viewer.me.useQuery({ includePasswordAdded: true }); - const orgBranding = useOrgBranding(); - const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN; - const isOrgAdminOrOwner = - orgBranding?.role === MembershipRole.ADMIN || orgBranding?.role === MembershipRole.OWNER; +export default async function SettingsLayoutAppDir(props: SettingsLayoutAppDir) { + const session = await getServerSession(AUTH_OPTIONS); + const userId = session?.user?.id ?? -1; + const orgId = session?.user?.org?.id ?? -1; + let currentOrg = null; + let otherTeams = null; - const processTabsMemod = useMemo(() => { - const processedTabs = tabs.map((tab) => { - if (tab.href === "/settings/my-account") { - return { - ...tab, - name: user?.name || "my_account", - icon: undefined, - avatar: getUserAvatarUrl(user), - }; - } else if (tab.href === "/settings/organizations") { - const newArray = (tab?.children ?? []).filter( - (child) => isOrgAdminOrOwner || !organizationAdminKeys.includes(child.name) - ); + try { + currentOrg = await OrganizationRepository.findCurrentOrg({ userId, orgId }); + } catch (err) {} - // TODO: figure out feature flag as it doesnt cause a re-render of the component when loaded. - // You have to refresh the page to see the changes. - if (true) { - newArray.splice(4, 0, { - name: "attributes", - href: "/settings/organizations/attributes", - }); - } - - return { - ...tab, - children: newArray, - name: orgBranding?.name || "organization", - avatar: getPlaceholderAvatar(orgBranding?.logoUrl, orgBranding?.name), - }; - } else if ( - tab.href === "/settings/security" && - user?.identityProvider === IdentityProvider.GOOGLE && - !user?.twoFactorEnabled && - !user?.passwordAdded - ) { - const filtered = tab?.children?.filter( - (childTab) => childTab.href !== "/settings/security/two-factor-auth" - ); - return { ...tab, children: filtered }; - } else if (tab.href === "/settings/developer") { - const filtered = tab?.children?.filter( - (childTab) => isOrgAdminOrOwner || childTab.name !== "admin_api" - ); - return { ...tab, children: filtered }; - } - return tab; + try { + otherTeams = await OrganizationRepository.findTeamsInOrgIamNotPartOf({ + userId, + parentId: orgId, }); + } catch (err) {} - // check if name is in adminRequiredKeys - return processedTabs.filter((tab) => { - if (organizationRequiredKeys.includes(tab.name)) return !!orgBranding; - if (tab.name === "other_teams" && !isOrgAdminOrOwner) return false; - - if (isAdmin) return true; - return !adminRequiredKeys.includes(tab.name); - }); - }, [isAdmin, orgBranding, isOrgAdminOrOwner, user]); - - return processTabsMemod; -}; - -const BackButtonInSidebar = ({ name }: { name: string }) => { - return ( - - - - {name} - - - ); -}; - -interface SettingsSidebarContainerProps { - className?: string; - navigationIsOpenedOnMobile?: boolean; - bannersHeight?: number; + return ; } -const TeamListCollapsible = () => { - const { data: teams } = trpc.viewer.teams.list.useQuery(); - const { t } = useLocale(); - const [teamMenuState, setTeamMenuState] = - useState<{ teamId: number | undefined; teamMenuOpen: boolean }[]>(); - const searchParams = useCompatSearchParams(); - 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]); - - return ( - <> - {teams && - teamMenuState && - teams.map((team, index: number) => { - if (!teamMenuState[index]) { - return null; - } - if (teamMenuState.some((teamState) => teamState.teamId === team.id)) - return ( - - setTeamMenuState([ - ...teamMenuState, - (teamMenuState[index] = { - ...teamMenuState[index], - teamMenuOpen: !teamMenuState[index].teamMenuOpen, - }), - ]) - }> - -
- setTeamMenuState([ - ...teamMenuState, - (teamMenuState[index] = { - ...teamMenuState[index], - teamMenuOpen: !teamMenuState[index].teamMenuOpen, - }), - ]) - }> -
- {teamMenuState[index].teamMenuOpen ? ( - - ) : ( - - )} -
- {!team.parentId && ( - {team.name - )} -

{team.name}

- {!team.accepted && ( - - Inv. - - )} -
-
- - {team.accepted && ( - - )} - - - {(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 */} - {/* */} - - {/* Hide if there is a parent ID */} - {!team.parentId ? ( - <> - - - ) : null} - - )} - -
- ); - })} - - ); -}; - -const SettingsSidebarContainer = ({ - className = "", - navigationIsOpenedOnMobile, - bannersHeight, -}: SettingsSidebarContainerProps) => { - const searchParams = useCompatSearchParams(); - const { t } = useLocale(); - const tabsWithPermissions = useTabs(); - const [otherTeamMenuState, setOtherTeamMenuState] = useState< - { - teamId: number | undefined; - teamMenuOpen: boolean; - }[] - >(); - 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(undefined, { - enabled: !!session.data?.user?.org, - }); - - // 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); - - return ( - - ); -}; - -const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) => { - const { t } = useLocale(); - const router = useRouter(); - - return ( - <> - - - ); -}; - -export default function SettingsLayout({ - children, - ...rest -}: { - children: React.ReactNode; -} & ComponentProps) { - const pathname = usePathname(); - const state = useState(false); - const [sideContainerOpen, setSideContainerOpen] = state; - - useEffect(() => { - const closeSideContainer = () => { - if (window.innerWidth >= 1024) { - setSideContainerOpen(false); - } - }; - - window.addEventListener("resize", closeSideContainer); - return () => { - window.removeEventListener("resize", closeSideContainer); - }; - }, []); - - useEffect(() => { - if (sideContainerOpen) { - setSideContainerOpen(!sideContainerOpen); - } - }, [pathname]); - - return ( - - } - drawerState={state} - MobileNavigationContainer={null} - TopNavContainer={ - setSideContainerOpen(!sideContainerOpen)} /> - }> -
-
- {children} -
-
-
- ); -} - -const SidebarContainerElement = ({ - sideContainerOpen, - bannersHeight, - setSideContainerOpen, -}: SidebarContainerElementProps) => { - const { t } = useLocale(); - return ( - <> - {/* Mobile backdrop */} - {sideContainerOpen && ( - - )} - - - ); -}; - -type SidebarContainerElementProps = { - sideContainerOpen: boolean; - bannersHeight?: number; - setSideContainerOpen: React.Dispatch>; -}; - -export const getLayout = (page: React.ReactElement) => {page}; +export const getLayout = async (page: React.ReactElement) => await SettingsLayoutAppDir({ children: page }); diff --git a/packages/features/settings/appDir/SettingsLayoutAppDirClient.tsx b/packages/features/settings/appDir/SettingsLayoutAppDirClient.tsx new file mode 100644 index 0000000000..491e8970fd --- /dev/null +++ b/packages/features/settings/appDir/SettingsLayoutAppDirClient.tsx @@ -0,0 +1,745 @@ +"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, { useEffect, useState, useMemo } 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 type { OrganizationRepository } from "@calcom/lib/server/repository/organization"; +import { IdentityProvider, MembershipRole, UserPermissionRole } from "@calcom/prisma/enums"; +import { trpc } from "@calcom/trpc/react"; +import type { VerticalTabItemProps } from "@calcom/ui"; +import { Badge, Button, ErrorBoundary, Icon, Skeleton, VerticalTabItem } from "@calcom/ui"; + +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" }, + { name: "out_of_office", href: "/settings/my-account/out-of-office" }, + // 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: "credit-card", + 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" }, + { name: "admin_api", href: "/settings/organizations/admin-api" }, + // 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: "privacy", + href: "/settings/organizations/privacy", + }, + { + name: "billing", + href: "/settings/organizations/billing", + }, + { name: "OAuth Clients", href: "/settings/organizations/platform/oauth-clients" }, + { + name: "SSO", + href: "/settings/organizations/sso", + }, + { + name: "directory_sync", + href: "/settings/organizations/dsync", + }, + { + name: "admin_api", + href: "https://cal.com/docs/enterprise-features/api/api-reference/bookings#admin-access", + }, + ], + }, + { + name: "teams", + href: "/teams", + icon: "users", + children: [], + }, + { + name: "other_teams", + href: "/settings/organizations/teams/other", + 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: "lockedSMS", href: "/settings/admin/lockedSMS" }, + { name: "oAuth", href: "/settings/admin/oAuth" }, + ], + }, +]; + +tabs.find((tab) => { + if (tab.name === "security" && !HOSTED_CAL_FEATURES) { + tab.children?.push({ name: "sso_configuration", href: "/settings/security/sso" }); + // TODO: Enable dsync for self hosters + // tab.children?.push({ name: "directory_sync", href: "/settings/security/dsync" }); + } +}); + +// The following keys are assigned to admin only +const adminRequiredKeys = ["admin"]; +const organizationRequiredKeys = ["organization"]; +const organizationAdminKeys = ["privacy", "billing", "OAuth Clients", "SSO", "directory_sync"]; + +const useTabs = () => { + const session = useSession(); + const { data: user } = trpc.viewer.me.useQuery({ includePasswordAdded: true }); + const orgBranding = useOrgBranding(); + const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN; + const isOrgAdminOrOwner = + orgBranding?.role === MembershipRole.ADMIN || orgBranding?.role === MembershipRole.OWNER; + + const processTabsMemod = useMemo(() => { + const processedTabs = tabs.map((tab) => { + if (tab.href === "/settings/my-account") { + return { + ...tab, + name: user?.name || "my_account", + icon: undefined, + avatar: getUserAvatarUrl(user), + }; + } else if (tab.href === "/settings/organizations") { + const newArray = (tab?.children ?? []).filter( + (child) => isOrgAdminOrOwner || !organizationAdminKeys.includes(child.name) + ); + + // TODO: figure out feature flag as it doesnt cause a re-render of the component when loaded. + // You have to refresh the page to see the changes. + if (true) { + newArray.splice(4, 0, { + name: "attributes", + href: "/settings/organizations/attributes", + }); + } + + return { + ...tab, + children: newArray, + name: orgBranding?.name || "organization", + avatar: getPlaceholderAvatar(orgBranding?.logoUrl, orgBranding?.name), + }; + } else if ( + tab.href === "/settings/security" && + user?.identityProvider === IdentityProvider.GOOGLE && + !user?.twoFactorEnabled && + !user?.passwordAdded + ) { + const filtered = tab?.children?.filter( + (childTab) => childTab.href !== "/settings/security/two-factor-auth" + ); + return { ...tab, children: filtered }; + } else if (tab.href === "/settings/developer") { + const filtered = tab?.children?.filter( + (childTab) => isOrgAdminOrOwner || childTab.name !== "admin_api" + ); + return { ...tab, children: filtered }; + } + return tab; + }); + + // check if name is in adminRequiredKeys + return processedTabs.filter((tab) => { + if (organizationRequiredKeys.includes(tab.name)) return !!orgBranding; + if (tab.name === "other_teams" && !isOrgAdminOrOwner) return false; + + if (isAdmin) return true; + return !adminRequiredKeys.includes(tab.name); + }); + }, [isAdmin, orgBranding, isOrgAdminOrOwner, user]); + + return processTabsMemod; +}; + +const BackButtonInSidebar = ({ name }: { name: string }) => { + return ( + + + + {name} + + + ); +}; + +interface SettingsSidebarContainerProps { + className?: string; + navigationIsOpenedOnMobile?: boolean; + bannersHeight?: number; + currentOrg: SettingsLayoutProps["currentOrg"]; + otherTeams: SettingsLayoutProps["otherTeams"]; +} + +const TeamListCollapsible = () => { + const { data: teams } = trpc.viewer.teams.list.useQuery(); + const { t } = useLocale(); + const [teamMenuState, setTeamMenuState] = + useState<{ teamId: number | undefined; teamMenuOpen: boolean }[]>(); + const searchParams = useCompatSearchParams(); + 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]); + + return ( + <> + {teams && + teamMenuState && + teams.map((team, index: number) => { + if (!teamMenuState[index]) { + return null; + } + if (teamMenuState.some((teamState) => teamState.teamId === team.id)) + return ( + + setTeamMenuState([ + ...teamMenuState, + (teamMenuState[index] = { + ...teamMenuState[index], + teamMenuOpen: !teamMenuState[index].teamMenuOpen, + }), + ]) + }> + +
+ setTeamMenuState([ + ...teamMenuState, + (teamMenuState[index] = { + ...teamMenuState[index], + teamMenuOpen: !teamMenuState[index].teamMenuOpen, + }), + ]) + }> +
+ {teamMenuState[index].teamMenuOpen ? ( + + ) : ( + + )} +
+ {!team.parentId && ( + {team.name + )} +

{team.name}

+ {!team.accepted && ( + + Inv. + + )} +
+
+ + {team.accepted && ( + + )} + + + {(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 */} + {/* */} + + {/* Hide if there is a parent ID */} + {!team.parentId ? ( + <> + + + ) : null} + + )} + +
+ ); + })} + + ); +}; + +const SettingsSidebarContainer = ({ + className = "", + navigationIsOpenedOnMobile, + bannersHeight, + currentOrg, + otherTeams, +}: SettingsSidebarContainerProps) => { + const searchParams = useCompatSearchParams(); + const { t } = useLocale(); + const tabsWithPermissions = useTabs(); + const [otherTeamMenuState, setOtherTeamMenuState] = useState< + { + teamId: number | undefined; + teamMenuOpen: boolean; + }[] + >(); + + // 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); + + return ( + + ); +}; + +const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) => { + const { t } = useLocale(); + const router = useRouter(); + + return ( + <> + + + ); +}; + +export type SettingsLayoutProps = { + children: React.ReactNode; + currentOrg: Awaited> | null; + otherTeams: Awaited> | null; +} & ComponentProps; + +export default function SettingsLayoutAppDirClient({ + children, + currentOrg, + otherTeams, + ...rest +}: SettingsLayoutProps) { + const pathname = usePathname(); + const state = useState(false); + const [sideContainerOpen, setSideContainerOpen] = state; + + useEffect(() => { + const closeSideContainer = () => { + if (window.innerWidth >= 1024) { + setSideContainerOpen(false); + } + }; + + window.addEventListener("resize", closeSideContainer); + return () => { + window.removeEventListener("resize", closeSideContainer); + }; + }, []); + + useEffect(() => { + if (sideContainerOpen) { + setSideContainerOpen(!sideContainerOpen); + } + }, [pathname]); + + return ( + + } + drawerState={state} + MobileNavigationContainer={null} + TopNavContainer={ + setSideContainerOpen(!sideContainerOpen)} /> + }> +
+
+ {children} +
+
+
+ ); +} + +const SidebarContainerElement = ({ + sideContainerOpen, + bannersHeight, + setSideContainerOpen, + currentOrg, + otherTeams, +}: SidebarContainerElementProps) => { + const { t } = useLocale(); + return ( + <> + {/* Mobile backdrop */} + {sideContainerOpen && ( + + )} + + + ); +}; + +type SidebarContainerElementProps = { + sideContainerOpen: boolean; + bannersHeight?: number; + setSideContainerOpen: React.Dispatch>; + currentOrg: SettingsLayoutProps["currentOrg"]; + otherTeams: SettingsLayoutProps["otherTeams"]; +}; diff --git a/packages/lib/server/repository/organization.ts b/packages/lib/server/repository/organization.ts index 177b0a3379..ffedd7db4f 100644 --- a/packages/lib/server/repository/organization.ts +++ b/packages/lib/server/repository/organization.ts @@ -194,6 +194,66 @@ export class OrganizationRepository { return getParsedTeam(org); } + static async findCurrentOrg({ userId, orgId }: { userId: number; orgId: number }) { + const membership = await prisma.membership.findFirst({ + where: { + userId, + team: { + id: orgId, + }, + }, + include: { + team: true, + }, + }); + + const organizationSettings = await prisma.organizationSettings.findUnique({ + where: { + organizationId: orgId, + }, + select: { + lockEventTypeCreationForUsers: true, + adminGetsNoSlotsNotification: true, + isAdminReviewed: true, + }, + }); + + if (!membership) { + throw new Error("You do not have a membership to your organization"); + } + + const metadata = teamMetadataSchema.parse(membership?.team.metadata); + + return { + canAdminImpersonate: !!organizationSettings?.isAdminReviewed, + organizationSettings: { + lockEventTypeCreationForUsers: organizationSettings?.lockEventTypeCreationForUsers, + adminGetsNoSlotsNotification: organizationSettings?.adminGetsNoSlotsNotification, + }, + user: { + role: membership?.role, + accepted: membership?.accepted, + }, + ...membership?.team, + metadata, + }; + } + + static async findTeamsInOrgIamNotPartOf({ userId, parentId }: { userId: number; parentId: number | null }) { + const teamsInOrgIamNotPartOf = await prisma.team.findMany({ + where: { + parentId, + members: { + none: { + userId, + }, + }, + }, + }); + + return teamsInOrgIamNotPartOf; + } + static async adminFindById({ id }: { id: number }) { const org = await prisma.team.findUnique({ where: { @@ -228,7 +288,6 @@ export class OrganizationRepository { }, }, }); - if (!org) { throw new Error("Organization not found"); } diff --git a/packages/trpc/server/routers/viewer/organizations/list.handler.ts b/packages/trpc/server/routers/viewer/organizations/list.handler.ts index d68258033e..ed1b255f56 100644 --- a/packages/trpc/server/routers/viewer/organizations/list.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/list.handler.ts @@ -1,5 +1,4 @@ -import type { PrismaClient } from "@calcom/prisma"; -import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; +import { OrganizationRepository } from "@calcom/lib/server/repository/organization"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import { TRPCError } from "@trpc/server"; @@ -7,7 +6,6 @@ import { TRPCError } from "@trpc/server"; type ListHandlerInput = { ctx: { user: NonNullable; - prisma: PrismaClient; }; }; @@ -16,52 +14,10 @@ export const listHandler = async ({ ctx }: ListHandlerInput) => { if (!ctx.user.organization?.id) { throw new TRPCError({ code: "BAD_REQUEST", message: "You do not belong to an organization" }); } - - const membership = await ctx.prisma.membership.findFirst({ - where: { - userId: ctx.user.id, - team: { - id: ctx.user.organization.id, - }, - }, - include: { - team: true, - }, + return await OrganizationRepository.findCurrentOrg({ + userId: ctx.user.id, + orgId: ctx.user.organization.id, }); - - const organizationSettings = await ctx.prisma.organizationSettings.findUnique({ - where: { - organizationId: ctx.user.organization.id, - }, - select: { - lockEventTypeCreationForUsers: true, - adminGetsNoSlotsNotification: true, - isAdminReviewed: true, - }, - }); - - if (!membership) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "You do not have a membership to your organization", - }); - } - - const metadata = teamMetadataSchema.parse(membership?.team.metadata); - - return { - canAdminImpersonate: !!organizationSettings?.isAdminReviewed, - organizationSettings: { - lockEventTypeCreationForUsers: organizationSettings?.lockEventTypeCreationForUsers, - adminGetsNoSlotsNotification: organizationSettings?.adminGetsNoSlotsNotification, - }, - user: { - role: membership?.role, - accepted: membership?.accepted, - }, - ...membership?.team, - metadata, - }; }; export default listHandler; diff --git a/packages/trpc/server/routers/viewer/organizations/listOtherTeams.handler.ts b/packages/trpc/server/routers/viewer/organizations/listOtherTeams.handler.ts index 6165b3d3cb..37b01461c5 100644 --- a/packages/trpc/server/routers/viewer/organizations/listOtherTeams.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/listOtherTeams.handler.ts @@ -1,4 +1,4 @@ -import { prisma } from "@calcom/prisma"; +import { OrganizationRepository } from "@calcom/lib/server/repository/organization"; import type { TrpcSessionUser } from "../../../trpc"; @@ -12,18 +12,11 @@ export const listOtherTeamHandler = async ({ ctx: { user } }: ListOptions) => { if (!user?.organization?.isOrgAdmin) { return []; } - const teamsInOrgIamNotPartOf = await prisma.team.findMany({ - where: { - parentId: user?.organization?.id ?? null, - members: { - none: { - userId: user.id, - }, - }, - }, - }); - return teamsInOrgIamNotPartOf; + return await OrganizationRepository.findTeamsInOrgIamNotPartOf({ + userId: user.id, + parentId: user?.organization?.id ?? null, + }); }; export default listOtherTeamHandler;