Teams skeleton loader (#3148)
* Teams skeleton loader * [id] skeleton * Fix mt Co-authored-by: Peer Richelsen <peeroke@gmail.com>pull/3146/head^2
parent
643b46aa8f
commit
13b5618a71
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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")}
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
Loading…
Reference in New Issue