perf: SSR - /availablilty/[schedule] (#20829)
* ssr for schedule page * improve skeleton * improve skeleton * improve skeleton
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export async function revalidateSchedulePage(scheduleId: number) {
|
||||
revalidatePath(`/availability/${scheduleId}`);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { AvailabilityScheduleSkeleton } from "./skeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <AvailabilityScheduleSkeleton />;
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { availabilityRouter } from "@calcom/trpc/server/routers/viewer/availability/_router";
|
||||
import { travelSchedulesRouter } from "@calcom/trpc/server/routers/viewer/travelSchedules/_router";
|
||||
|
||||
import { AvailabilitySettingsWebWrapper } from "~/availability/[schedule]/schedule-view";
|
||||
|
||||
const querySchema = z.object({
|
||||
@@ -29,8 +33,25 @@ const Page = async ({ params }: PageProps) => {
|
||||
if (!parsed.success) {
|
||||
notFound();
|
||||
}
|
||||
const scheduleId = parsed.data.schedule;
|
||||
|
||||
return <AvailabilitySettingsWebWrapper />;
|
||||
const [availabilityCaller, travelSchedulesCaller] = await Promise.all([
|
||||
createRouterCaller(availabilityRouter),
|
||||
createRouterCaller(travelSchedulesRouter),
|
||||
]);
|
||||
|
||||
const [scheduleData, travelSchedulesData] = await Promise.all([
|
||||
availabilityCaller.schedule.get({ scheduleId }),
|
||||
travelSchedulesCaller.get(),
|
||||
]);
|
||||
|
||||
if (!scheduleData) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<AvailabilitySettingsWebWrapper scheduleData={scheduleData} travelSchedulesData={travelSchedulesData} />
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { SkeletonText, SkeletonContainer, SkeletonButton } from "@calcom/ui/components/skeleton";
|
||||
|
||||
export function AvailabilityScheduleSkeleton() {
|
||||
return (
|
||||
<Shell withoutMain={true}>
|
||||
<SkeletonContainer>
|
||||
<div className="max-w-full">
|
||||
{/* Header section */}
|
||||
<div className="bg-default sticky top-0 z-10 mb-0 flex items-center py-2 md:mb-6 md:mt-0 lg:mb-9">
|
||||
{/* Back button */}
|
||||
<div className="flex items-center">
|
||||
<SkeletonText className="h-6 w-6 rounded-md ltr:mr-2 rtl:ml-2" /> {/* Back button */}
|
||||
</div>
|
||||
|
||||
{/* Title section */}
|
||||
<header className="flex w-full max-w-full items-center truncate">
|
||||
<div className="w-full truncate ltr:mr-4 rtl:ml-4 md:block">
|
||||
<SkeletonText className="font-cal max-w-28 sm:max-w-72 md:max-w-80 hidden h-6 truncate text-xl font-semibold tracking-wide md:block" />
|
||||
{/* Title */}
|
||||
<SkeletonText className="mt-1 hidden h-4 w-64 text-sm md:block" /> {/* Subtitle */}
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="relative flex-shrink-0 md:relative md:bottom-auto md:right-auto">
|
||||
{/* Desktop buttons - Set to Default, Delete, and Save */}
|
||||
<div className="flex items-center justify-end">
|
||||
{/* Set to Default - desktop only */}
|
||||
<div className="hidden items-center rounded-md sm:flex">
|
||||
<SkeletonText className="h-4 w-28" /> {/* "Set to Default" text */}
|
||||
<div className="ml-2 h-5 w-11 rounded-full bg-gray-200" /> {/* Toggle */}
|
||||
<div className="mx-3 h-4 w-px bg-gray-200" /> {/* Divider */}
|
||||
</div>
|
||||
{/* Delete button */}
|
||||
<SkeletonButton className="mr-2 hidden h-9 w-9 rounded-md sm:inline-flex" />
|
||||
{/* Second divider */}
|
||||
<div className="mx-3 hidden h-4 w-px bg-gray-200 sm:inline" /> {/* Divider */}
|
||||
{/* Save button - updated to match screenshot */}
|
||||
<SkeletonButton className="h-9 w-20 rounded-md bg-gray-200" /> {/* Save button */}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="mt-4 w-full md:mt-0">
|
||||
<div className="flex flex-col sm:mx-0 xl:flex-row xl:space-x-6">
|
||||
<div className="flex-1 flex-row xl:mr-0 xl:w-[75%]">
|
||||
{/* Schedule container */}
|
||||
<div className="border-subtle mb-6 rounded-md border">
|
||||
<div className="flex flex-col gap-4 p-2 sm:p-4">
|
||||
{/* Days of the week */}
|
||||
{Array(7)
|
||||
.fill(0)
|
||||
.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex w-full flex-col gap-4 last:mb-0 sm:flex-row sm:gap-6 sm:px-0">
|
||||
{/* Day toggle and name */}
|
||||
<div className="flex h-[36px] items-center justify-between sm:w-32">
|
||||
<div>
|
||||
<div className="text-default flex flex-row items-center space-x-2 rtl:space-x-reverse">
|
||||
<div className="mr-2 h-6 w-11 rounded-full bg-gray-200" /> {/* Toggle */}
|
||||
<SkeletonText className="h-5 w-16 text-sm" /> {/* Day name */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Time inputs - conditional rendering based on day being active */}
|
||||
{i !== 0 &&
|
||||
i !== 6 && ( // Only show time inputs for Monday-Friday
|
||||
<div className="flex sm:gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-1 last:mb-0 sm:gap-2">
|
||||
<div className="flex flex-row gap-2 sm:gap-3">
|
||||
<SkeletonText className="h-9 w-[90px] rounded-md sm:w-[100px]" />{" "}
|
||||
{/* Time input */}
|
||||
<span className="text-default w-2 self-center"> - </span>
|
||||
<SkeletonText className="h-9 w-[90px] rounded-md sm:w-[100px]" />{" "}
|
||||
{/* Time input */}
|
||||
</div>
|
||||
<SkeletonText className="h-9 w-9 rounded-md" /> {/* Add button */}
|
||||
</div>
|
||||
</div>
|
||||
<SkeletonText className="h-9 w-9 rounded-md" /> {/* Copy button */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date overrides section */}
|
||||
<div className="p-6">
|
||||
<div className="flex items-center">
|
||||
<SkeletonText className="h-5 w-32 font-medium" /> {/* Date overrides */}
|
||||
<SkeletonText className="ml-2 h-4 w-4 rounded-full" /> {/* Info icon */}
|
||||
</div>
|
||||
<SkeletonText className="mb-4 mt-1 h-4 w-full max-w-md text-sm" /> {/* Description */}
|
||||
<div className="space-y-2">
|
||||
<SkeletonButton className="h-9 w-36 rounded-md" /> {/* Add an override button */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar - only visible on desktop */}
|
||||
<div className="min-w-40 col-span-3 hidden space-y-2 md:block lg:col-span-1 xl:w-[25%]">
|
||||
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
|
||||
<div>
|
||||
<SkeletonText className="mb-1 h-4 w-24 text-sm" /> {/* Timezone */}
|
||||
<SkeletonText className="mt-1 h-9 w-72 rounded-md" /> {/* Timezone selector */}
|
||||
</div>
|
||||
|
||||
<hr className="border-subtle my-6 mr-8" />
|
||||
|
||||
<div className="rounded-md">
|
||||
<SkeletonText className="h-5 w-64 text-sm" /> {/* Something doesn't look right? */}
|
||||
<div className="mt-3 flex">
|
||||
<SkeletonButton className="h-9 w-40 rounded-md" /> {/* Launch troubleshooter */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { revalidateSchedulePage } from "app/(use-page-wrapper)/availability/[schedule]/actions";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import { AvailabilitySettings } from "@calcom/atoms/availability/AvailabilitySettings";
|
||||
import type { BulkUpdatParams } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
|
||||
import { withErrorFromUnknown } from "@calcom/lib/getClientErrorFromUnknown";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import type { ScheduleRepository } from "@calcom/lib/server/repository/schedule";
|
||||
import type { TravelScheduleRepository } from "@calcom/lib/server/repository/travelSchedule";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
type PageProps = {
|
||||
scheduleFetched?: Awaited<ReturnType<typeof ScheduleRepository.findDetailedScheduleById>>;
|
||||
travelSchedules?: Awaited<ReturnType<typeof TravelScheduleRepository.findTravelSchedulesByUserId>>;
|
||||
scheduleData: RouterOutputs["viewer"]["availability"]["schedule"]["get"];
|
||||
travelSchedulesData: RouterOutputs["viewer"]["travelSchedules"]["get"];
|
||||
};
|
||||
|
||||
export const AvailabilitySettingsWebWrapper = ({
|
||||
scheduleFetched: scheduleProp,
|
||||
travelSchedules: travelSchedulesProp,
|
||||
scheduleData: schedule,
|
||||
travelSchedulesData: travelSchedules,
|
||||
}: PageProps) => {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const searchParams = useSearchParams();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const utils = trpc.useUtils();
|
||||
const me = useMeQuery();
|
||||
const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1;
|
||||
const fromEventType = searchParams?.get("fromEventType");
|
||||
const scheduleId = schedule.id;
|
||||
const { timeFormat } = me.data || { timeFormat: null };
|
||||
const { data: scheduleData, isPending: isFetchingPending } = trpc.viewer.availability.schedule.get.useQuery(
|
||||
{ scheduleId },
|
||||
{
|
||||
enabled: !!scheduleId && !scheduleProp,
|
||||
}
|
||||
);
|
||||
const isPending = isFetchingPending && !scheduleProp;
|
||||
const schedule = scheduleProp ?? scheduleData;
|
||||
|
||||
const { data: travelSchedulesData } = trpc.viewer.travelSchedules.get.useQuery(undefined, {
|
||||
enabled: !travelSchedulesProp,
|
||||
});
|
||||
const travelSchedules = travelSchedulesProp ?? travelSchedulesData;
|
||||
|
||||
const [isBulkUpdateModalOpen, setIsBulkUpdateModalOpen] = useState(false);
|
||||
const bulkUpdateDefaultAvailabilityMutation =
|
||||
trpc.viewer.availability.schedule.bulkUpdateToDefaultAvailability.useMutation();
|
||||
@@ -57,7 +42,7 @@ export const AvailabilitySettingsWebWrapper = ({
|
||||
bulkUpdateDefaultAvailabilityMutation.mutate(
|
||||
{
|
||||
eventTypeIds,
|
||||
selectedDefaultScheduleId: scheduleData?.id,
|
||||
selectedDefaultScheduleId: scheduleId,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -86,6 +71,7 @@ export const AvailabilitySettingsWebWrapper = ({
|
||||
}
|
||||
}
|
||||
utils.viewer.availability.schedule.get.invalidate({ scheduleId: data.schedule.id });
|
||||
revalidateSchedulePage(scheduleId);
|
||||
utils.viewer.availability.list.invalidate();
|
||||
showToast(
|
||||
t("availability_updated_successfully", {
|
||||
@@ -115,20 +101,12 @@ export const AvailabilitySettingsWebWrapper = ({
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: reimplement Skeletons for this page in here
|
||||
if (isPending) return null;
|
||||
|
||||
// We wait for the schedule to be loaded before rendering the form inside AvailabilitySettings
|
||||
// since `defaultValues` cannot be redeclared after first render and using `values` will
|
||||
// trigger a form reset when revalidating. Introducing flaky behavior.
|
||||
if (!schedule) return null;
|
||||
|
||||
return (
|
||||
<AvailabilitySettings
|
||||
schedule={schedule}
|
||||
travelSchedules={isDefaultSchedule ? travelSchedules || [] : []}
|
||||
isDeleting={deleteMutation.isPending}
|
||||
isLoading={isPending}
|
||||
isLoading={false}
|
||||
isSaving={updateMutation.isPending}
|
||||
enableOverrides={true}
|
||||
timeFormat={timeFormat}
|
||||
|
||||
Reference in New Issue
Block a user