Teams skeleton loader (#3148)

* Teams skeleton loader

* [id] skeleton

* Fix mt

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
pull/3146/head^2
sean-brydon 2022-06-24 23:44:49 +01:00 committed by GitHub
parent 643b46aa8f
commit 13b5618a71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 184 additions and 113 deletions

View File

@ -0,0 +1,42 @@
import React from "react";
import { SkeletonText } from "@calcom/ui";
import { ShellSubHeading } from "@components/Shell";
function SkeletonLoaderTeamList({ className }: { className?: string }) {
return (
<>
<ul className="-mx-4 animate-pulse divide-y divide-neutral-200 rounded-sm border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
</ul>
</>
);
}
export default SkeletonLoaderTeamList;
function SkeletonItem() {
return (
<li className="group flex w-full items-center justify-between px-3 py-4">
<div className="flex-grow truncate text-sm">
<div className="flex justify-start space-x-2 px-2">
<SkeletonText width="10" height="10" className="rounded-full"></SkeletonText>
<div className="space-y-2">
<SkeletonText height="4" width="32"></SkeletonText>
<SkeletonText height="4" width="16"></SkeletonText>
</div>
</div>
</div>
<div className="mt-4 hidden flex-shrink-0 pr-4 sm:mt-0 sm:ml-5 lg:flex">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
<SkeletonText width="12" height="4"></SkeletonText>
<SkeletonText width="4" height="4"></SkeletonText>
<SkeletonText width="4" height="4"></SkeletonText>
</div>
</div>
</li>
);
}

View File

@ -4,15 +4,16 @@ import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import showToast from "@calcom/lib/notification"; import showToast from "@calcom/lib/notification";
import { SkeletonAvatar, SkeletonText } from "@calcom/ui";
import { Alert } from "@calcom/ui/Alert"; import { Alert } from "@calcom/ui/Alert";
import { Button } from "@calcom/ui/Button"; import { Button } from "@calcom/ui/Button";
import SAMLConfiguration from "@ee/components/saml/Configuration"; import SAMLConfiguration from "@ee/components/saml/Configuration";
import { QueryCell } from "@lib/QueryCell";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar"; import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import { trpc } from "@lib/trpc"; import { trpc } from "@lib/trpc";
import Loader from "@components/Loader";
import Shell from "@components/Shell"; import Shell from "@components/Shell";
import MemberInvitationModal from "@components/team/MemberInvitationModal"; import MemberInvitationModal from "@components/team/MemberInvitationModal";
import MemberList from "@components/team/MemberList"; import MemberList from "@components/team/MemberList";
@ -37,121 +38,151 @@ export function TeamSettingsPage() {
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false); const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false);
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
const { data: team, isLoading } = trpc.useQuery(["viewer.teams.get", { teamId: Number(router.query.id) }], { const query = trpc.useQuery(["viewer.teams.get", { teamId: Number(router.query.id) }], {
onError: (e) => { onError: (e) => {
setErrorMessage(e.message); setErrorMessage(e.message);
}, },
}); });
const isAdmin =
team && (team.membership.role === MembershipRole.OWNER || team.membership.role === MembershipRole.ADMIN);
return ( return (
<Shell <QueryCell
backPath={!errorMessage ? `/settings/teams` : undefined} query={query}
heading={team?.name} loading={() => {
subtitle={team && t("manage_this_team")} return (
HeadingLeftIcon={ <Shell
team && ( backPath={!errorMessage ? `/settings/teams` : undefined}
<Avatar heading={
size={12} <div className="pt-2">
imageSrc={getPlaceholderAvatar(team?.logo, team?.name as string)} <SkeletonText width="16" height="6" />
alt="Team Logo" </div>
className="mt-1" }
/> subtitle={<SkeletonText width="12" height="4" />}
) HeadingLeftIcon={<SkeletonAvatar width="12" height="12" className="mt-1" />}>
}> <>
{!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />} <div className="block sm:flex md:max-w-5xl">
{isLoading && <Loader />} <div className="w-full ltr:mr-2 rtl:ml-2 sm:w-9/12">
{team && ( <div className="-mx-0 h-[531px] rounded-sm border border-neutral-200 bg-white px-4 sm:px-6"></div>
<> <div className="mb-3 mt-7 flex items-center justify-between">
<div className="block sm:flex md:max-w-5xl"> <SkeletonText width="12" height="4"></SkeletonText>
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-9/12"> </div>
{team.membership.role === MembershipRole.OWNER && <div className="-mx-0 h-16 rounded-sm border border-neutral-200 bg-white px-4 sm:px-6"></div>
team.membership.isMissingSeat && </div>
team.requiresUpgrade ? ( </div>
<Alert </>
severity="warning" </Shell>
title={t("hidden_team_member_title")} );
message={ }}
<> success={({ data: team }) => {
{t("hidden_team_owner_message")} <UpgradeToFlexibleProModal teamId={team.id} /> const isAdmin =
{/* <a href={"https://cal.com/upgrade"} className="underline"> team &&
{"https://cal.com/upgrade"} (team.membership.role === MembershipRole.OWNER || team.membership.role === MembershipRole.ADMIN);
</a> */} return (
</> <Shell
} backPath={!errorMessage ? `/settings/teams` : undefined}
className="mb-4 " heading={team?.name}
subtitle={team && t("manage_this_team")}
HeadingLeftIcon={
team && (
<Avatar
size={12}
imageSrc={getPlaceholderAvatar(team?.logo, team?.name as string)}
alt="Team Logo"
className="mt-1"
/> />
) : ( )
<> }>
{team.membership.isMissingSeat && ( {!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />}
<Alert {team && (
severity="warning" <>
title={t("hidden_team_member_title")} <div className="block sm:flex md:max-w-5xl">
message={t("hidden_team_member_message")} <div className="w-full ltr:mr-2 rtl:ml-2 sm:w-9/12">
className="mb-4 " {team.membership.role === MembershipRole.OWNER &&
/> team.membership.isMissingSeat &&
)} team.requiresUpgrade ? (
{team.membership.role === MembershipRole.OWNER && team.requiresUpgrade && ( <Alert
<Alert severity="warning"
severity="warning" title={t("hidden_team_member_title")}
title={t("upgrade_to_flexible_pro_title")} message={
message={ <>
<span> {t("hidden_team_owner_message")} <UpgradeToFlexibleProModal teamId={team.id} />
{t("upgrade_to_flexible_pro_message")} <br /> {/* <a href={"https://cal.com/upgrade"} className="underline">
<UpgradeToFlexibleProModal teamId={team.id} /> {"https://cal.com/upgrade"}
</span> </a> */}
} </>
className="mb-4" }
/> className="mb-4 "
)} />
</> ) : (
)} <>
{team.membership.isMissingSeat && (
<Alert
severity="warning"
title={t("hidden_team_member_title")}
message={t("hidden_team_member_message")}
className="mb-4 "
/>
)}
{team.membership.role === MembershipRole.OWNER && team.requiresUpgrade && (
<Alert
severity="warning"
title={t("upgrade_to_flexible_pro_title")}
message={
<span>
{t("upgrade_to_flexible_pro_message")} <br />
<UpgradeToFlexibleProModal teamId={team.id} />
</span>
}
className="mb-4"
/>
)}
</>
)}
<div className="-mx-0 rounded-sm border border-neutral-200 bg-white px-4 sm:px-6"> <div className="-mx-0 rounded-sm border border-neutral-200 bg-white px-4 sm:px-6">
{isAdmin ? ( {isAdmin ? (
<TeamSettings team={team} /> <TeamSettings team={team} />
) : ( ) : (
<div className="py-5"> <div className="py-5">
<span className="mb-1 font-bold">{t("team_info")}</span> <span className="mb-1 font-bold">{t("team_info")}</span>
<p className="text-sm text-gray-700">{team.bio}</p> <p className="text-sm text-gray-700">{team.bio}</p>
</div>
)}
</div>
<div className="mb-3 mt-7 flex items-center justify-between">
<h3 className="font-cal text-xl leading-6 text-gray-900">{t("members")}</h3>
{isAdmin && (
<div className="relative flex items-center">
<Button
type="button"
color="secondary"
StartIcon={PlusIcon}
onClick={() => setShowMemberInvitationModal(true)}
data-testid="new-member-button">
{t("new_member")}
</Button>
</div>
)}
</div>
<MemberList team={team} members={team.members || []} />
{isAdmin && <SAMLConfiguration teamsView={true} teamId={team.id} />}
</div> </div>
)} <div className="min-w-32 mt-8 w-full px-2 ltr:ml-2 rtl:mr-2 sm:mt-0 md:w-3/12">
</div> <TeamSettingsRightSidebar role={team.membership.role} team={team} />
<div className="mb-3 mt-7 flex items-center justify-between">
<h3 className="font-cal text-xl leading-6 text-gray-900">{t("members")}</h3>
{isAdmin && (
<div className="relative flex items-center">
<Button
type="button"
color="secondary"
StartIcon={PlusIcon}
onClick={() => setShowMemberInvitationModal(true)}
data-testid="new-member-button">
{t("new_member")}
</Button>
</div> </div>
</div>
{showMemberInvitationModal && (
<MemberInvitationModal
isOpen={showMemberInvitationModal}
team={team}
currentMember={team.membership.role}
onExit={() => setShowMemberInvitationModal(false)}
/>
)} )}
</div> </>
<MemberList team={team} members={team.members || []} /> )}
{isAdmin && <SAMLConfiguration teamsView={true} teamId={team.id} />} </Shell>
</div> );
<div className="min-w-32 mt-8 w-full px-2 ltr:ml-2 rtl:mr-2 sm:mt-0 md:w-3/12"> }}></QueryCell>
<TeamSettingsRightSidebar role={team.membership.role} team={team} />
</div>
</div>
{showMemberInvitationModal && (
<MemberInvitationModal
isOpen={showMemberInvitationModal}
team={team}
currentMember={team.membership.role}
onExit={() => setShowMemberInvitationModal(false)}
/>
)}
</>
)}
</Shell>
); );
} }

View File

@ -12,15 +12,14 @@ import EmptyScreen from "@calcom/ui/EmptyScreen";
import useMeQuery from "@lib/hooks/useMeQuery"; import useMeQuery from "@lib/hooks/useMeQuery";
import { trpc } from "@lib/trpc"; import { trpc } from "@lib/trpc";
import Loader from "@components/Loader";
import SettingsShell from "@components/SettingsShell"; import SettingsShell from "@components/SettingsShell";
import SkeletonLoaderTeamList from "@components/team/SkeletonloaderTeamList";
import TeamCreateModal from "@components/team/TeamCreateModal"; import TeamCreateModal from "@components/team/TeamCreateModal";
import TeamList from "@components/team/TeamList"; import TeamList from "@components/team/TeamList";
function Teams() { function Teams() {
const { t } = useLocale(); const { t } = useLocale();
const { status } = useSession(); const { status } = useSession();
const loading = status === "loading";
const [showCreateTeamModal, setShowCreateTeamModal] = useState(false); const [showCreateTeamModal, setShowCreateTeamModal] = useState(false);
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
@ -32,8 +31,6 @@ function Teams() {
}, },
}); });
if (loading) return <Loader />;
const teams = data?.filter((m) => m.accepted) || []; const teams = data?.filter((m) => m.accepted) || [];
const invites = data?.filter((m) => !m.accepted) || []; const invites = data?.filter((m) => !m.accepted) || [];
const isFreePlan = me.data?.plan === "FREE"; const isFreePlan = me.data?.plan === "FREE";
@ -78,7 +75,8 @@ function Teams() {
<TeamList teams={invites}></TeamList> <TeamList teams={invites}></TeamList>
</div> </div>
)} )}
{!isLoading && !teams.length && ( {isLoading && <SkeletonLoaderTeamList />}
{!teams.length && !isLoading && (
<EmptyScreen <EmptyScreen
Icon={UserGroupIcon} Icon={UserGroupIcon}
headline={t("no_teams")} headline={t("no_teams")}

View File

@ -8,8 +8,8 @@ type SkeletonBaseProps = {
interface AvatarProps extends SkeletonBaseProps { interface AvatarProps extends SkeletonBaseProps {
// Limit this cause we don't use avatars bigger than thi // Limit this cause we don't use avatars bigger than thi
width: "2" | "3" | "4" | "5" | "6" | "8"; width: "2" | "3" | "4" | "5" | "6" | "8" | "12";
height: "2" | "3" | "4" | "5" | "6" | "8"; height: "2" | "3" | "4" | "5" | "6" | "8" | "12";
} }
interface SkeletonContainer { interface SkeletonContainer {
@ -24,7 +24,7 @@ const SkeletonAvatar: React.FC<AvatarProps> = ({ width, height }) => {
}; };
const SkeletonText: React.FC<SkeletonBaseProps> = ({ width, height }) => { const SkeletonText: React.FC<SkeletonBaseProps> = ({ width, height }) => {
return <div className={`rounded-md bg-gray-200 w-${width} h-${height} ${classNames}`} />; return <div className={classNames(`rounded-md bg-gray-200 w-${width} h-${height}`, classNames)} />;
}; };
const SkeletonButton: React.FC<SkeletonBaseProps> = ({ width, height }) => { const SkeletonButton: React.FC<SkeletonBaseProps> = ({ width, height }) => {