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:
Hariom Balhara
2022-09-05 19:06:34 +00:00
committed by GitHub
co-authored by Peer Richelsen zomars
parent 10815c9541
commit 52b2ce8774
12 changed files with 66 additions and 65 deletions
@@ -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"
+11 -7
View File
@@ -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} />;
};
};
+1 -5
View File
@@ -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}
+1 -5
View File
@@ -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>
+1 -1
View File
@@ -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">
+1 -2
View File
@@ -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 }) => (
+1 -5
View File
@@ -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>
+1 -4
View File
@@ -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} />
+6 -4
View File
@@ -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
)}
/>
+31 -22
View File
@@ -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>
+2 -2
View File
@@ -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>
+6 -4
View File
@@ -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
)}
/>