V2.0 Shell - Progressive Rendering with Skeleton (#4138)
* Fix Routing * Update up and down icons * Add Shell Progressive loading * Cleanup of custom loader * Remove skeleton for heading and subtitle * Replaces isI18nLoading for isLocaleReady Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
co-authored by
Peer Richelsen
zomars
parent
10815c9541
commit
52b2ce8774
@@ -7,7 +7,6 @@ import { useForm } from "react-hook-form";
|
||||
import type { z } from "zod";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
|
||||
@@ -49,9 +48,8 @@ interface CreateEventTypeBtnProps {
|
||||
}
|
||||
|
||||
export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
||||
const { t } = useLocale();
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
// URL encoded params
|
||||
const teamId: number | undefined =
|
||||
typeof router.query.teamId === "string" && router.query.teamId
|
||||
@@ -139,7 +137,9 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
||||
{ shallow: true }
|
||||
);
|
||||
};
|
||||
|
||||
if (!isLocaleReady) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Dialog
|
||||
name="new-eventtype"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { ReactNode } from "react";
|
||||
import {
|
||||
QueryObserverIdleResult,
|
||||
QueryObserverLoadingErrorResult,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
UseQueryResult,
|
||||
} from "react-query";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import type { UseTRPCQueryOptions } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -32,7 +33,7 @@ interface QueryCellOptionsBase<TData, TError extends ErrorLike> {
|
||||
error?: (
|
||||
query: QueryObserverLoadingErrorResult<TData, TError> | QueryObserverRefetchErrorResult<TData, TError>
|
||||
) => JSXElementOrNull;
|
||||
loading?: (query: QueryObserverLoadingResult<TData, TError>) => JSXElementOrNull;
|
||||
loading?: (query: QueryObserverLoadingResult<TData, TError> | null) => JSXElementOrNull;
|
||||
idle?: (query: QueryObserverIdleResult<TData, TError>) => JSXElementOrNull;
|
||||
}
|
||||
|
||||
@@ -61,12 +62,20 @@ export function QueryCell<TData, TError extends ErrorLike>(
|
||||
opts: QueryCellOptionsNoEmpty<TData, TError> | QueryCellOptionsWithEmpty<TData, TError>
|
||||
) {
|
||||
const { query } = opts;
|
||||
const { isLocaleReady } = useLocale();
|
||||
const StatusLoader = opts.customLoader || <Loader />; // Fixes edge case where this can return null form query cell
|
||||
|
||||
if (query.status === "loading" || !isLocaleReady) {
|
||||
return opts.loading?.(query.status === "loading" ? query : null) ?? StatusLoader;
|
||||
}
|
||||
|
||||
if (query.status === "success") {
|
||||
if ("empty" in opts && (query.data == null || (Array.isArray(query.data) && query.data.length === 0))) {
|
||||
return opts.empty(query);
|
||||
}
|
||||
return opts.success(query as any);
|
||||
}
|
||||
|
||||
if (query.status === "error") {
|
||||
return (
|
||||
opts.error?.(query) ?? (
|
||||
@@ -74,11 +83,7 @@ export function QueryCell<TData, TError extends ErrorLike>(
|
||||
)
|
||||
);
|
||||
}
|
||||
const StatusLoader = opts.customLoader || <Loader />; // Fixes edge case where this can return null form query cell
|
||||
|
||||
if (query.status === "loading") {
|
||||
return opts.loading?.(query) ?? StatusLoader;
|
||||
}
|
||||
if (query.status === "idle") {
|
||||
return opts.idle?.(query) ?? StatusLoader;
|
||||
}
|
||||
@@ -114,7 +119,6 @@ const withQuery = <TPath extends keyof TQueryValues & string>(
|
||||
>
|
||||
) {
|
||||
const query = trpc.useQuery(pathAndInput, params);
|
||||
|
||||
return <QueryCell query={query} {...opts} />;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -143,11 +143,7 @@ export default function IntegrationsPage() {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.useQuery(["viewer.integrations", { onlyInstalled: true }]);
|
||||
return (
|
||||
<Shell
|
||||
heading={t("installed_apps")}
|
||||
subtitle={t("manage_your_connected_apps")}
|
||||
large
|
||||
customLoader={<SkeletonLoader />}>
|
||||
<Shell heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")} large>
|
||||
<AppsShell>
|
||||
<QueryCell
|
||||
query={query}
|
||||
|
||||
@@ -59,11 +59,7 @@ export default function AvailabilityPage() {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div>
|
||||
<Shell
|
||||
heading={t("availability")}
|
||||
subtitle={t("configure_availability")}
|
||||
CTA={<NewScheduleButton />}
|
||||
customLoader={<SkeletonLoader />}>
|
||||
<Shell heading={t("availability")} subtitle={t("configure_availability")} CTA={<NewScheduleButton />}>
|
||||
<WithQuery success={({ data }) => <AvailabilityList {...data} />} customLoader={<SkeletonLoader />} />
|
||||
</Shell>
|
||||
</div>
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function Bookings() {
|
||||
return true;
|
||||
};
|
||||
return (
|
||||
<Shell heading={t("bookings")} subtitle={t("bookings_description")} customLoader={<SkeletonLoader />}>
|
||||
<Shell heading={t("bookings")} subtitle={t("bookings_description")}>
|
||||
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
|
||||
<BookingsShell>
|
||||
<div className="-mx-4 flex flex-col sm:mx-auto">
|
||||
|
||||
@@ -564,8 +564,7 @@ const EventTypesPage = () => {
|
||||
<Shell
|
||||
heading={t("event_types_page_title") as string}
|
||||
subtitle={t("event_types_page_subtitle") as string}
|
||||
CTA={<CTA />}
|
||||
customLoader={<SkeletonLoader />}>
|
||||
CTA={<CTA />}>
|
||||
<WithQuery
|
||||
customLoader={<SkeletonLoader />}
|
||||
success={({ data }) => (
|
||||
|
||||
@@ -60,11 +60,7 @@ export default function AvailabilityPage() {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div>
|
||||
<Shell
|
||||
heading={t("availability")}
|
||||
subtitle={t("configure_availability")}
|
||||
CTA={<NewScheduleButton />}
|
||||
customLoader={<SkeletonLoader />}>
|
||||
<Shell heading={t("availability")} subtitle={t("configure_availability")} CTA={<NewScheduleButton />}>
|
||||
<WithQuery success={({ data }) => <AvailabilityList {...data} />} customLoader={<SkeletonLoader />} />
|
||||
</Shell>
|
||||
</div>
|
||||
|
||||
@@ -80,10 +80,7 @@ export default function Bookings() {
|
||||
return true;
|
||||
};
|
||||
return (
|
||||
<BookingLayout
|
||||
heading={t("bookings")}
|
||||
subtitle={t("bookings_description")}
|
||||
customLoader={<SkeletonLoader />}>
|
||||
<BookingLayout heading={t("bookings")} subtitle={t("bookings_description")}>
|
||||
<div className="flex w-full flex-1 flex-col">
|
||||
{query.status === "error" && (
|
||||
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
type SkeletonBaseProps = {
|
||||
width: string;
|
||||
height: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
@@ -29,11 +29,13 @@ const SkeletonAvatar: React.FC<AvatarProps> = ({ width, height, className }) =>
|
||||
);
|
||||
};
|
||||
|
||||
const SkeletonText: React.FC<SkeletonBaseProps> = ({ width, height, className }) => {
|
||||
const SkeletonText: React.FC<SkeletonBaseProps> = ({ width = "", height = "", className = "" }) => {
|
||||
className = width ? `${className} w-${width}` : className;
|
||||
className = height ? `${className} h-${height}` : className;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
`dark:white-300 animate-pulse rounded-md bg-gray-300 dark:bg-gray-300/50 w-${width} h-${height}`,
|
||||
`dark:white-300 animate-pulse rounded-md bg-gray-300 empty:before:inline-block empty:before:content-[''] dark:bg-gray-300/50 `,
|
||||
className
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -26,16 +26,15 @@ import Dropdown, {
|
||||
DropdownMenuTrigger,
|
||||
} from "@calcom/ui/Dropdown";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { Loader } from "@calcom/ui/v2";
|
||||
import { useViewerI18n } from "@calcom/web/components/I18nLanguageHandler";
|
||||
|
||||
/* TODO: Get this from endpoint */
|
||||
import pkg from "../../../../apps/web/package.json";
|
||||
import ErrorBoundary from "../../ErrorBoundary";
|
||||
import { KBarRoot, KBarContent, KBarTrigger } from "../../Kbar";
|
||||
import { KBarContent, KBarRoot, KBarTrigger } from "../../Kbar";
|
||||
import Logo from "../../Logo";
|
||||
// TODO: re-introduce in 2.1 import Tips from "../modules/tips/Tips";
|
||||
import HeadSeo from "./head-seo";
|
||||
import { SkeletonText } from "./skeleton";
|
||||
|
||||
/* TODO: Migate this */
|
||||
|
||||
@@ -160,7 +159,6 @@ type LayoutProps = {
|
||||
// use when content needs to expand with flex
|
||||
flexChildrenContainer?: boolean;
|
||||
isPublic?: boolean;
|
||||
customLoader?: ReactNode;
|
||||
withoutMain?: boolean;
|
||||
};
|
||||
|
||||
@@ -174,7 +172,6 @@ export default function Shell(props: LayoutProps) {
|
||||
useRedirectToOnboardingIfNeeded();
|
||||
useTheme("light");
|
||||
const { session } = useRedirectToLoginIfUnauthenticated(props.isPublic);
|
||||
|
||||
if (!session && !props.isPublic) return null;
|
||||
|
||||
return (
|
||||
@@ -400,6 +397,9 @@ const navigation: NavigationItemType[] = [
|
||||
name: "Routing Forms",
|
||||
href: "/apps/routing_forms/forms",
|
||||
icon: Icon.FiFileText,
|
||||
isCurrent: ({ router }) => {
|
||||
return router.asPath.startsWith("/apps/routing_forms/");
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "workflows",
|
||||
@@ -463,7 +463,7 @@ const NavigationItem: React.FC<{
|
||||
isChild?: boolean;
|
||||
}> = (props) => {
|
||||
const { item, isChild } = props;
|
||||
const { t } = useLocale();
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
const router = useRouter();
|
||||
const isCurrent: NavigationItemType["isCurrent"] = item.isCurrent || defaultIsCurrent;
|
||||
const current = isCurrent({ isChild: !!isChild, item, router });
|
||||
@@ -490,7 +490,11 @@ const NavigationItem: React.FC<{
|
||||
aria-current={current ? "page" : undefined}
|
||||
/>
|
||||
)}
|
||||
<span className="hidden lg:inline">{t(item.name)}</span>
|
||||
{!isLocaleReady ? (
|
||||
<SkeletonText className="h-3 w-32" />
|
||||
) : (
|
||||
<span className="hidden lg:inline">{t(item.name)}</span>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
{item.child &&
|
||||
@@ -535,10 +539,11 @@ const MobileNavigationItem: React.FC<{
|
||||
}> = (props) => {
|
||||
const { item, itemIdx, isChild } = props;
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
const isCurrent: NavigationItemType["isCurrent"] = item.isCurrent || defaultIsCurrent;
|
||||
const current = isCurrent({ isChild: !!isChild, item, router });
|
||||
const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(props.item);
|
||||
|
||||
if (!shouldDisplayNavigationItem) return null;
|
||||
return (
|
||||
<Link key={item.name} href={item.href}>
|
||||
@@ -556,7 +561,11 @@ const MobileNavigationItem: React.FC<{
|
||||
aria-current={current ? "page" : undefined}
|
||||
/>
|
||||
)}
|
||||
<span className="block truncate">{t(item.name)}</span>
|
||||
{!isLocaleReady ? (
|
||||
<span className="block truncate">{t(item.name)}</span>
|
||||
) : (
|
||||
<SkeletonText className="" />
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
@@ -590,6 +599,8 @@ function SideBarContainer() {
|
||||
}
|
||||
|
||||
function SideBar() {
|
||||
const { isLocaleReady } = useLocale();
|
||||
|
||||
return (
|
||||
<aside className="hidden w-14 flex-col border-r border-gray-100 bg-gray-50 md:flex lg:w-56 lg:flex-shrink-0 lg:px-4">
|
||||
<div className="flex h-0 flex-1 flex-col overflow-y-auto pt-3 pb-4 lg:pt-5">
|
||||
@@ -614,7 +625,7 @@ function SideBar() {
|
||||
<Tips />
|
||||
*/}
|
||||
|
||||
<TrialBanner />
|
||||
{!isLocaleReady ? null : <TrialBanner />}
|
||||
<div data-testid="user-dropdown-trigger">
|
||||
<span className="hidden lg:inline">
|
||||
<UserDropdown />
|
||||
@@ -630,7 +641,7 @@ function SideBar() {
|
||||
|
||||
export function ShellMain(props: LayoutProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const { isLocaleReady } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-baseline">
|
||||
@@ -643,17 +654,15 @@ export function ShellMain(props: LayoutProps) {
|
||||
{props.heading && (
|
||||
<div className={classNames(props.large && "py-8", "flex w-full items-center px-2 pt-4 md:p-0")}>
|
||||
{props.HeadingLeftIcon && <div className="ltr:mr-4">{props.HeadingLeftIcon}</div>}
|
||||
<div className="mb-4 w-full">
|
||||
<>
|
||||
{props.heading && (
|
||||
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-black">
|
||||
{props.heading}
|
||||
</h1>
|
||||
)}
|
||||
{props.subtitle && (
|
||||
<p className="text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{props.subtitle}</p>
|
||||
)}
|
||||
</>
|
||||
<div className="mb-4 w-full ltr:mr-4 rtl:ml-4">
|
||||
{props.heading && (
|
||||
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-black">
|
||||
{!isLocaleReady ? null : props.heading}
|
||||
</h1>
|
||||
)}
|
||||
{props.subtitle && (
|
||||
<p className="text-sm text-neutral-500">{!isLocaleReady ? null : props.subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
{props.CTA && <div className="mb-4 flex-shrink-0">{props.CTA}</div>}
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function FormCard({
|
||||
{moveUp?.check() ? (
|
||||
<button
|
||||
type="button"
|
||||
className="invisible absolute left-0 -ml-[13px] -mt-0 mb-4 hidden h-7 w-7 scale-0 rounded-full border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100 sm:block"
|
||||
className="invisible absolute left-0 -ml-[13px] mb-4 flex h-6 w-6 scale-0 items-center justify-center rounded-md border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100 "
|
||||
onClick={() => moveUp?.fn()}>
|
||||
<Icon.FiArrowUp />
|
||||
</button>
|
||||
@@ -36,7 +36,7 @@ export default function FormCard({
|
||||
{moveDown?.check() ? (
|
||||
<button
|
||||
type="button"
|
||||
className="invisible absolute left-0 mt-8 -ml-[13px] hidden h-7 w-7 scale-0 rounded-full border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100 sm:block"
|
||||
className="invisible absolute left-0 -ml-[13px] mt-8 flex h-6 w-6 scale-0 items-center justify-center rounded-md border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow group-hover:visible group-hover:scale-100"
|
||||
onClick={() => moveDown?.fn()}>
|
||||
<Icon.FiArrowDown />
|
||||
</button>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
type SkeletonBaseProps = {
|
||||
width: string;
|
||||
height: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
@@ -29,11 +29,13 @@ const SkeletonAvatar: React.FC<AvatarProps> = ({ width, height, className }) =>
|
||||
);
|
||||
};
|
||||
|
||||
const SkeletonText: React.FC<SkeletonBaseProps> = ({ width, height, className }) => {
|
||||
const SkeletonText: React.FC<SkeletonBaseProps> = ({ width = "", height = "", className = "" }) => {
|
||||
className = width ? `${className} w-${width}` : className;
|
||||
className = height ? `${className} h-${height}` : className;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
`dark:white-300 animate-pulse rounded-md bg-gray-300 w-${width} h-${height}`,
|
||||
`dark:white-300 animate-pulse rounded-md bg-gray-300 empty:before:inline-block empty:before:content-[''] w-${width} h-${height}`,
|
||||
className
|
||||
)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user