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
This commit is contained in:
+25
-10
@@ -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<number, Record<keyof AppFlags, boolean>> | 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<typeof Shell>;
|
||||
|
||||
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
|
||||
<SidebarContainerElement
|
||||
sideContainerOpen={sideContainerOpen}
|
||||
setSideContainerOpen={setSideContainerOpen}
|
||||
teamFeatures={teamFeatures}
|
||||
/>
|
||||
}
|
||||
drawerState={state}
|
||||
@@ -778,10 +790,18 @@ export default function SettingsLayoutAppDirClient({ children, ...rest }: Settin
|
||||
);
|
||||
}
|
||||
|
||||
type SidebarContainerElementProps = {
|
||||
sideContainerOpen: boolean;
|
||||
bannersHeight?: number;
|
||||
setSideContainerOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
teamFeatures?: TeamFeatures;
|
||||
};
|
||||
|
||||
const SidebarContainerElement = ({
|
||||
sideContainerOpen,
|
||||
bannersHeight,
|
||||
setSideContainerOpen,
|
||||
teamFeatures,
|
||||
}: SidebarContainerElementProps) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
@@ -797,13 +817,8 @@ const SidebarContainerElement = ({
|
||||
<SettingsSidebarContainer
|
||||
navigationIsOpenedOnMobile={sideContainerOpen}
|
||||
bannersHeight={bannersHeight}
|
||||
teamFeatures={teamFeatures}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type SidebarContainerElementProps = {
|
||||
sideContainerOpen: boolean;
|
||||
bannersHeight?: number;
|
||||
setSideContainerOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
@@ -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<number, Record<keyof AppFlags, boolean>> | 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 (
|
||||
<>
|
||||
<SettingsLayoutAppDirClient {...props} />
|
||||
<SettingsLayoutAppDirClient {...props} teamFeatures={teamFeatures} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<Record<keyof AppFlags, boolean>>((acc, feature) => {
|
||||
acc[feature.slug as keyof AppFlags] = true;
|
||||
return acc;
|
||||
}, {} as Record<keyof AppFlags, boolean>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a feature is enabled globally in the application.
|
||||
* @param slug - The feature flag identifier to check
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { AppFlags } from "../config";
|
||||
|
||||
type TeamFeatures = Record<number, Record<keyof AppFlags, boolean>> | null;
|
||||
|
||||
export const useIsFeatureEnabledForTeam = ({
|
||||
teamFeatures,
|
||||
teamId,
|
||||
feature,
|
||||
}: {
|
||||
teamFeatures?: TeamFeatures;
|
||||
teamId?: number;
|
||||
feature: keyof AppFlags;
|
||||
}) => {
|
||||
if (!teamId || !teamFeatures?.[teamId]) return false;
|
||||
return teamFeatures[teamId][feature];
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user