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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user