From 1dca160db08ad102a44ceea81155ac2475093ce6 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Wed, 21 May 2025 13:41:45 +0100 Subject: [PATCH] perf: use app dir to fetch team feature flags in settings (#21433) * use app dir to fetch team feature flags * guard against type error if teamId does not exist in teamfeatures * remove redundant router --- .../SettingsLayoutAppDirClient.tsx | 35 +++++++++++++------ .../settings/(settings-layout)/layout.tsx | 17 ++++++++- .../features/flags/features.repository.ts | 22 ++++++++++++ .../flags/hooks/useIsFeatureEnabledForTeam.ts | 16 +++++++++ .../viewer/delegationCredential/_router.ts | 8 ----- 5 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 packages/features/flags/hooks/useIsFeatureEnabledForTeam.ts diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx index 4901b8b0c2..1275a84464 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/SettingsLayoutAppDirClient.tsx @@ -10,6 +10,8 @@ import React, { useEffect, useState, useMemo } from "react"; import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; import type { OrganizationBranding } from "@calcom/features/ee/organizations/context/provider"; +import type { AppFlags } from "@calcom/features/flags/config"; +import { useIsFeatureEnabledForTeam } from "@calcom/features/flags/hooks/useIsFeatureEnabledForTeam"; import Shell from "@calcom/features/shell/Shell"; import { HOSTED_CAL_FEATURES, IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; @@ -27,6 +29,8 @@ import type { VerticalTabItemProps } from "@calcom/ui/components/navigation"; import { VerticalTabItem } from "@calcom/ui/components/navigation"; import { Skeleton } from "@calcom/ui/components/skeleton"; +type TeamFeatures = Record> | null; + const getTabs = (orgBranding: OrganizationBranding | null) => { const tabs: VerticalTabItemProps[] = [ { @@ -272,6 +276,7 @@ interface SettingsSidebarContainerProps { className?: string; navigationIsOpenedOnMobile?: boolean; bannersHeight?: number; + teamFeatures?: TeamFeatures; } const TeamListCollapsible = () => { @@ -438,6 +443,7 @@ const SettingsSidebarContainer = ({ className = "", navigationIsOpenedOnMobile, bannersHeight, + teamFeatures, }: SettingsSidebarContainerProps) => { const searchParams = useCompatSearchParams(); const orgBranding = useOrgBranding(); @@ -449,11 +455,15 @@ const SettingsSidebarContainer = ({ }[] >(); const session = useSession(); - const { data } = trpc.viewer.delegationCredential.check.useQuery(undefined, { - enabled: !!session.data?.user?.org, + + const isDelegationCredentialEnabled = useIsFeatureEnabledForTeam({ + teamFeatures, + teamId: session.data?.user?.org?.id, + feature: "delegation-credential", }); + const tabsWithPermissions = useTabs({ - isDelegationCredentialEnabled: data?.hasDelegationCredential ?? false, + isDelegationCredentialEnabled, }); const { data: otherTeams } = trpc.viewer.organizations.listOtherTeams.useQuery(undefined, { @@ -727,9 +737,10 @@ const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) => export type SettingsLayoutProps = { children: React.ReactNode; containerClassName?: string; + teamFeatures?: TeamFeatures; } & ComponentProps; -export default function SettingsLayoutAppDirClient({ children, ...rest }: SettingsLayoutProps) { +export default function SettingsLayoutAppDirClient({ children, teamFeatures, ...rest }: SettingsLayoutProps) { const pathname = usePathname(); const state = useState(false); const [sideContainerOpen, setSideContainerOpen] = state; @@ -761,6 +772,7 @@ export default function SettingsLayoutAppDirClient({ children, ...rest }: Settin } drawerState={state} @@ -778,10 +790,18 @@ export default function SettingsLayoutAppDirClient({ children, ...rest }: Settin ); } +type SidebarContainerElementProps = { + sideContainerOpen: boolean; + bannersHeight?: number; + setSideContainerOpen: React.Dispatch>; + teamFeatures?: TeamFeatures; +}; + const SidebarContainerElement = ({ sideContainerOpen, bannersHeight, setSideContainerOpen, + teamFeatures, }: SidebarContainerElementProps) => { const { t } = useLocale(); return ( @@ -797,13 +817,8 @@ const SidebarContainerElement = ({ ); }; - -type SidebarContainerElementProps = { - sideContainerOpen: boolean; - bannersHeight?: number; - setSideContainerOpen: React.Dispatch>; -}; diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx index 4e558930ba..a731c4426b 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/layout.tsx @@ -3,6 +3,8 @@ import { redirect } from "next/navigation"; import React from "react"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import type { AppFlags } from "@calcom/features/flags/config"; +import { FeaturesRepository } from "@calcom/features/flags/features.repository"; import { buildLegacyRequest } from "@lib/buildLegacyCtx"; @@ -16,9 +18,22 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) { return redirect("/auth/login"); } + let teamFeatures: Record> | null = null; + + // For now we only grab organization features but it would be nice to fetch these on the server side for specific team feature flags + if (session?.user.org) { + const featuresRepository = new FeaturesRepository(); + const features = await featuresRepository.getTeamFeatures(session.user.org.id); + if (features) { + teamFeatures = { + [session.user.org.id]: features, + }; + } + } + return ( <> - + ); } diff --git a/packages/features/flags/features.repository.ts b/packages/features/flags/features.repository.ts index 149c0b6359..e183b51e0f 100644 --- a/packages/features/flags/features.repository.ts +++ b/packages/features/flags/features.repository.ts @@ -1,6 +1,7 @@ import { Prisma } from "@prisma/client"; import { captureException } from "@sentry/nextjs"; +import kysely from "@calcom/kysely"; import db from "@calcom/prisma"; import type { AppFlags } from "./config"; @@ -58,6 +59,27 @@ export class FeaturesRepository implements IFeaturesRepository { }, {} as AppFlags); } + /** + * Gets all features enabled for a specific team in a map format. + * @param teamId - The ID of the team to get features for + * @returns Promise<{ [slug: string]: boolean } | null> + */ + public async getTeamFeatures(teamId: number) { + const result = await kysely + .selectFrom("TeamFeatures") + .innerJoin("Feature", "Feature.slug", "TeamFeatures.featureId") + .select(["Feature.slug", "Feature.enabled"]) + .where("TeamFeatures.teamId", "=", teamId) + .execute(); + + if (!result.length) return null; + + return result.reduce>((acc, feature) => { + acc[feature.slug as keyof AppFlags] = true; + return acc; + }, {} as Record); + } + /** * Checks if a feature is enabled globally in the application. * @param slug - The feature flag identifier to check diff --git a/packages/features/flags/hooks/useIsFeatureEnabledForTeam.ts b/packages/features/flags/hooks/useIsFeatureEnabledForTeam.ts new file mode 100644 index 0000000000..73809b81ae --- /dev/null +++ b/packages/features/flags/hooks/useIsFeatureEnabledForTeam.ts @@ -0,0 +1,16 @@ +import type { AppFlags } from "../config"; + +type TeamFeatures = Record> | null; + +export const useIsFeatureEnabledForTeam = ({ + teamFeatures, + teamId, + feature, +}: { + teamFeatures?: TeamFeatures; + teamId?: number; + feature: keyof AppFlags; +}) => { + if (!teamId || !teamFeatures?.[teamId]) return false; + return teamFeatures[teamId][feature]; +}; diff --git a/packages/trpc/server/routers/viewer/delegationCredential/_router.ts b/packages/trpc/server/routers/viewer/delegationCredential/_router.ts index fb124173fd..2b53311b0d 100644 --- a/packages/trpc/server/routers/viewer/delegationCredential/_router.ts +++ b/packages/trpc/server/routers/viewer/delegationCredential/_router.ts @@ -44,14 +44,6 @@ const checkDelegationCredentialFeature = async ({ }; export const delegationCredentialRouter = router({ - check: authedOrgAdminProcedure.query(async (opts) => { - return await checkDelegationCredentialFeature({ - ctx: opts.ctx, - next: async () => ({ - hasDelegationCredential: true, - }), - }); - }), list: authedOrgAdminProcedure.use(checkDelegationCredentialFeature).query(async (opts) => { const handler = await import("./list.handler").then((mod) => mod.default); return handler(opts);