perf: server-side fetching for /availability (#20804)

* availability

* refactor

* add loader

* improve

* update

* fix loader

* rename

* satisfying types

* refactor

* remove log

* add CTA to skeleton

* refactor

* fix revalidations

* refactor

* update

* finish
This commit is contained in:
Benny Joo
2025-04-24 11:10:54 +02:00
committed by GitHub
parent 91ab27e070
commit 6815f2f9c7
9 changed files with 124 additions and 88 deletions
@@ -0,0 +1,7 @@
"use server";
import { revalidatePath } from "next/cache";
export async function revalidateAvailabilityList() {
revalidatePath("/availability");
}
@@ -0,0 +1,5 @@
import AvailabilityLoader from "app/(use-page-wrapper)/(main-nav)/availability/skeleton";
export default function Loading() {
return <AvailabilityLoader />;
}
@@ -1,11 +1,14 @@
import { createRouterCaller } from "app/_trpc/context";
import type { PageProps } from "app/_types";
import { _generateMetadata, getTranslate } from "app/_utils";
// import { cookies, headers } from "next/headers";
import { notFound } from "next/navigation";
// import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
// import { buildLegacyRequest } from "@lib/buildLegacyCtx";
// import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import AvailabilityPage, { AvailabilityCTA } from "~/availability/availability-view";
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import { availabilityRouter } from "@calcom/trpc/server/routers/viewer/availability/_router";
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
import { AvailabilityList, AvailabilityCTA } from "~/availability/availability-view";
import { ShellMainAppDir } from "../ShellMainAppDir";
@@ -19,29 +22,44 @@ export const generateMetadata = async () => {
);
};
const Page = async () => {
// const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
// const userId = session?.user?.id;
// const orgId = session?.user?.org?.id;
// if (!userId || !orgId) {
// notFound();
// }
const Page = async ({ searchParams: _searchParams }: PageProps) => {
const searchParams = await _searchParams;
const t = await getTranslate();
try {
// const currentOrg = await OrganizationRepository.findCurrentOrg({
// orgId,
// userId,
// });
const t = await getTranslate();
const [meCaller, availabilityCaller] = await Promise.all([
createRouterCaller(meRouter),
createRouterCaller(availabilityRouter),
]);
const [me, availabilities] = await Promise.all([meCaller.get(), availabilityCaller.list()]);
const organizationId = me.organization?.id ?? me.profiles[0]?.organizationId;
const isOrgPrivate = organizationId
? await OrganizationRepository.checkIfPrivate({
orgId: organizationId,
})
: false;
const canViewTeamAvailability = me.organization?.isOrgAdmin || !isOrgPrivate;
return (
<ShellMainAppDir
heading={t("availability")}
subtitle={t("configure_availability")}
CTA={<AvailabilityCTA />}>
<AvailabilityPage />
CTA={
<AvailabilityCTA
toggleGroupOptions={[
{ value: "mine", label: t("my_availability") },
...(canViewTeamAvailability ? [{ value: "team", label: t("team_availability") }] : []),
]}
/>
}>
{searchParams?.type === "team" && canViewTeamAvailability ? (
<AvailabilitySliderTable userTimeFormat={me?.timeFormat ?? null} isOrg={!!organizationId} />
) : (
<AvailabilityList availabilities={availabilities ?? { schedules: [] }} me={me} />
)}
</ShellMainAppDir>
);
} catch {
} catch (error) {
notFound();
}
};
@@ -0,0 +1,25 @@
"use client";
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
import SkeletonLoader from "@calcom/features/availability/components/SkeletonLoader";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { AvailabilityCTA } from "~/availability/availability-view";
export default function AvailabilityLoader() {
const { t } = useLocale();
const toggleGroupOptions = [
{ value: "mine", label: t("my_availability") },
{ value: "team", label: t("team_availability") },
];
return (
<ShellMainAppDir
heading={t("availability")}
subtitle={t("configure_availability")}
CTA={<AvailabilityCTA toggleGroupOptions={toggleGroupOptions} />}>
<SkeletonLoader />
</ShellMainAppDir>
);
}
@@ -1,5 +1,6 @@
"use client";
import { revalidateAvailabilityList } from "app/(use-page-wrapper)/(main-nav)/availability/actions";
import { revalidateSchedulePage } from "app/(use-page-wrapper)/availability/[schedule]/actions";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
@@ -47,6 +48,7 @@ export const AvailabilitySettingsWebWrapper = ({
{
onSuccess: () => {
utils.viewer.availability.list.invalidate();
revalidateAvailabilityList();
callback();
showToast(t("success"), "success");
},
@@ -73,6 +75,7 @@ export const AvailabilitySettingsWebWrapper = ({
utils.viewer.availability.schedule.get.invalidate({ scheduleId: data.schedule.id });
revalidateSchedulePage(scheduleId);
utils.viewer.availability.list.invalidate();
revalidateAvailabilityList();
showToast(
t("availability_updated_successfully", {
scheduleName: data.schedule.name,
@@ -97,6 +100,7 @@ export const AvailabilitySettingsWebWrapper = ({
},
onSuccess: () => {
showToast(t("schedule_deleted_successfully"), "success");
revalidateAvailabilityList();
router.push("/availability");
},
});
@@ -1,33 +1,31 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { revalidateAvailabilityList } from "app/(use-page-wrapper)/(main-nav)/availability/actions";
import Link from "next/link";
import { useRouter, usePathname } from "next/navigation";
import { useCallback, useState } from "react";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import SkeletonLoader from "@calcom/features/availability/components/SkeletonLoader";
import { BulkEditDefaultForEventsModal } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
import type { BulkUpdatParams } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import type { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { ToggleGroup } from "@calcom/ui/components/form";
import { showToast } from "@calcom/ui/components/toast";
export function AvailabilityList() {
type AvailabilityListProps = {
me: RouterOutputs["viewer"]["me"]["get"];
availabilities: RouterOutputs["viewer"]["availability"]["list"];
};
export function AvailabilityList({ availabilities, me }: AvailabilityListProps) {
const { t } = useLocale();
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
const utils = trpc.useUtils();
const { data: availabilityData } = trpc.viewer.availability.list.useQuery();
const meQuery = trpc.viewer.me.get.useQuery();
const router = useRouter();
const deleteMutation = trpc.viewer.availability.schedule.delete.useMutation({
@@ -55,6 +53,7 @@ export function AvailabilityList() {
utils.viewer.availability.list.invalidate();
},
onSuccess: () => {
revalidateAvailabilityList();
showToast(t("schedule_deleted_successfully"), "success");
},
});
@@ -62,6 +61,7 @@ export function AvailabilityList() {
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
onSuccess: async ({ schedule }) => {
await utils.viewer.availability.list.invalidate();
revalidateAvailabilityList();
showToast(
t("availability_updated_successfully", {
scheduleName: schedule.name,
@@ -92,6 +92,7 @@ export function AvailabilityList() {
{
onSuccess: () => {
utils.viewer.availability.list.invalidate();
revalidateAvailabilityList();
showToast(t("success"), "success");
callback();
},
@@ -120,13 +121,9 @@ export function AvailabilityList() {
const [animationParentRef] = useAutoAnimate<HTMLUListElement>();
if (!availabilityData) {
return <SkeletonLoader />;
}
return (
<>
{availabilityData.schedules.length === 0 ? (
{availabilities.schedules.length === 0 ? (
<div className="flex justify-center">
<EmptyScreen
Icon="clock"
@@ -140,16 +137,16 @@ export function AvailabilityList() {
<>
<div className="border-subtle bg-default overflow-hidden rounded-md border">
<ul className="divide-subtle divide-y" data-testid="schedules" ref={animationParentRef}>
{availabilityData.schedules.map((schedule) => (
{availabilities.schedules.map((schedule) => (
<ScheduleListItem
displayOptions={{
hour12: meQuery.data?.timeFormat ? meQuery.data.timeFormat === 12 : undefined,
timeZone: meQuery.data?.timeZone,
weekStart: meQuery.data?.weekStart || "Sunday",
hour12: me?.timeFormat ? me.timeFormat === 12 : undefined,
timeZone: me?.timeZone,
weekStart: me?.weekStart || "Sunday",
}}
key={schedule.id}
schedule={schedule}
isDeletable={availabilityData.schedules.length !== 1}
isDeletable={availabilities.schedules.length !== 1}
updateDefault={updateMutation.mutate}
deleteFunction={deleteMutation.mutate}
duplicateFunction={duplicateMutation.mutate}
@@ -181,12 +178,13 @@ export function AvailabilityList() {
);
}
type PageProps = {
currentOrg?: Awaited<ReturnType<typeof OrganizationRepository.findCurrentOrg>> | null;
type AvailabilityCTAProps = {
toggleGroupOptions: {
value: string;
label: string;
}[];
};
export const AvailabilityCTA = () => {
const { t } = useLocale();
export const AvailabilityCTA = ({ toggleGroupOptions }: AvailabilityCTAProps) => {
const searchParams = useCompatSearchParams();
const router = useRouter();
const pathname = usePathname();
@@ -203,18 +201,6 @@ export const AvailabilityCTA = () => {
[searchParams]
);
const { data } = trpc.viewer.organizations.listCurrent.useQuery();
const isOrgAdminOrOwner = (data && checkAdminOrOwner(data.user.role)) ?? false;
const isOrgAndPrivate = data?.isOrganization && data.isPrivate;
const canViewTeamAvailability = isOrgAdminOrOwner || !isOrgAndPrivate;
const toggleGroupOptions = [{ value: "mine", label: t("my_availability") }];
if (canViewTeamAvailability) {
toggleGroupOptions.push({ value: "team", label: t("team_availability") });
}
return (
<div className="flex items-center gap-2">
<ToggleGroup
@@ -230,30 +216,3 @@ export const AvailabilityCTA = () => {
</div>
);
};
export default function AvailabilityPage({ currentOrg }: PageProps) {
const { t } = useLocale();
const searchParams = useCompatSearchParams();
const me = useMeQuery();
const { data: _data } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { enabled: !currentOrg });
const data = currentOrg ?? _data;
const isOrg = Boolean(data);
const isOrgAdminOrOwner = (data && checkAdminOrOwner(data.user.role)) ?? false;
const isOrgAndPrivate = data?.isOrganization && data.isPrivate;
const canViewTeamAvailability = isOrgAdminOrOwner || !isOrgAndPrivate;
const toggleGroupOptions = [{ value: "mine", label: t("my_availability") }];
if (canViewTeamAvailability) {
toggleGroupOptions.push({ value: "team", label: t("team_availability") });
}
return searchParams?.get("type") === "team" && canViewTeamAvailability ? (
<AvailabilitySliderTable userTimeFormat={me?.data?.timeFormat ?? null} isOrg={isOrg} />
) : (
<AvailabilityList />
);
}
@@ -1,6 +1,8 @@
import { SkeletonText } from "@calcom/ui/components/skeleton";
import { Button } from "@calcom/ui/components/button";
"use client";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { SkeletonText } from "@calcom/ui/components/skeleton";
function SkeletonLoader() {
return (
@@ -1,3 +1,5 @@
"use client";
import Link from "next/link";
import { Fragment } from "react";
@@ -5,6 +7,8 @@ import { availabilityAsString } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { sortAvailabilityStrings } from "@calcom/lib/weekstart";
import type { RouterOutputs } from "@calcom/trpc/react";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import {
Dropdown,
DropdownItem,
@@ -12,8 +16,6 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@calcom/ui/components/dropdown";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { Icon } from "@calcom/ui/components/icon";
import { showToast } from "@calcom/ui/components/toast";
@@ -456,4 +456,18 @@ export class OrganizationRepository {
},
});
}
static async checkIfPrivate({ orgId }: { orgId: number }) {
const team = await prisma.team.findUnique({
where: {
id: orgId,
isOrganization: true,
},
select: {
isPrivate: true,
},
});
return team?.isPrivate ?? false;
}
}