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 showToast from "@calcom/lib/notification";
import { SkeletonAvatar, SkeletonText } from "@calcom/ui";
import { Alert } from "@calcom/ui/Alert";
import { Button } from "@calcom/ui/Button";
import SAMLConfiguration from "@ee/components/saml/Configuration";
import { QueryCell } from "@lib/QueryCell";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
import { useLocale } from "@lib/hooks/useLocale";
import { trpc } from "@lib/trpc";
import Loader from "@components/Loader";
import Shell from "@components/Shell";
import MemberInvitationModal from "@components/team/MemberInvitationModal";
import MemberList from "@components/team/MemberList";
@ -37,121 +38,151 @@ export function TeamSettingsPage() {
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false);
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) => {
setErrorMessage(e.message);
},
});
const isAdmin =
team && (team.membership.role === MembershipRole.OWNER || team.membership.role === MembershipRole.ADMIN);
return (
<Shell
backPath={!errorMessage ? `/settings/teams` : undefined}
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"
/>
)
}>
{!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />}
{isLoading && <Loader />}
{team && (
<>
<div className="block sm:flex md:max-w-5xl">
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-9/12">
{team.membership.role === MembershipRole.OWNER &&
team.membership.isMissingSeat &&
team.requiresUpgrade ? (
<Alert
severity="warning"
title={t("hidden_team_member_title")}
message={
<>
{t("hidden_team_owner_message")} <UpgradeToFlexibleProModal teamId={team.id} />
{/* <a href={"https://cal.com/upgrade"} className="underline">
{"https://cal.com/upgrade"}
</a> */}
</>
}
className="mb-4 "
<QueryCell
query={query}
loading={() => {
return (
<Shell
backPath={!errorMessage ? `/settings/teams` : undefined}
heading={
<div className="pt-2">
<SkeletonText width="16" height="6" />
</div>
}
subtitle={<SkeletonText width="12" height="4" />}
HeadingLeftIcon={<SkeletonAvatar width="12" height="12" className="mt-1" />}>
<>
<div className="block sm:flex md:max-w-5xl">
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-9/12">
<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">
<SkeletonText width="12" height="4"></SkeletonText>
</div>
<div className="-mx-0 h-16 rounded-sm border border-neutral-200 bg-white px-4 sm:px-6"></div>
</div>
</div>
</>
</Shell>
);
}}
success={({ data: team }) => {
const isAdmin =
team &&
(team.membership.role === MembershipRole.OWNER || team.membership.role === MembershipRole.ADMIN);
return (
<Shell
backPath={!errorMessage ? `/settings/teams` : undefined}
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 && (
<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"
/>
)}
</>
)}
)
}>
{!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />}
{team && (
<>
<div className="block sm:flex md:max-w-5xl">
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-9/12">
{team.membership.role === MembershipRole.OWNER &&
team.membership.isMissingSeat &&
team.requiresUpgrade ? (
<Alert
severity="warning"
title={t("hidden_team_member_title")}
message={
<>
{t("hidden_team_owner_message")} <UpgradeToFlexibleProModal teamId={team.id} />
{/* <a href={"https://cal.com/upgrade"} className="underline">
{"https://cal.com/upgrade"}
</a> */}
</>
}
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">
{isAdmin ? (
<TeamSettings team={team} />
) : (
<div className="py-5">
<span className="mb-1 font-bold">{t("team_info")}</span>
<p className="text-sm text-gray-700">{team.bio}</p>
<div className="-mx-0 rounded-sm border border-neutral-200 bg-white px-4 sm:px-6">
{isAdmin ? (
<TeamSettings team={team} />
) : (
<div className="py-5">
<span className="mb-1 font-bold">{t("team_info")}</span>
<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="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 className="min-w-32 mt-8 w-full px-2 ltr:ml-2 rtl:mr-2 sm:mt-0 md:w-3/12">
<TeamSettingsRightSidebar role={team.membership.role} team={team} />
</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} />}
</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">
<TeamSettingsRightSidebar role={team.membership.role} team={team} />
</div>
</div>
{showMemberInvitationModal && (
<MemberInvitationModal
isOpen={showMemberInvitationModal}
team={team}
currentMember={team.membership.role}
onExit={() => setShowMemberInvitationModal(false)}
/>
)}
</>
)}
</Shell>
</>
)}
</Shell>
);
}}></QueryCell>
);
}

View File

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

View File

@ -8,8 +8,8 @@ type SkeletonBaseProps = {
interface AvatarProps extends SkeletonBaseProps {
// Limit this cause we don't use avatars bigger than thi
width: "2" | "3" | "4" | "5" | "6" | "8";
height: "2" | "3" | "4" | "5" | "6" | "8";
width: "2" | "3" | "4" | "5" | "6" | "8" | "12";
height: "2" | "3" | "4" | "5" | "6" | "8" | "12";
}
interface SkeletonContainer {
@ -24,7 +24,7 @@ const SkeletonAvatar: React.FC<AvatarProps> = ({ 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 }) => {