Reusable Upgrade component for teams only features (#6473)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
pull/6483/head
Carina Wollendorfer 2023-01-13 19:48:19 -05:00 committed by GitHub
parent 443329b99f
commit 1376f0991f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 80 additions and 72 deletions

View File

@ -7,7 +7,6 @@ import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Badge,
Button,
ColorPicker,
Form,
@ -17,6 +16,7 @@ import {
SkeletonContainer,
SkeletonText,
Switch,
UpgradeTeamsBadge,
} from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
@ -49,6 +49,7 @@ const AppearanceView = () => {
const session = useSession();
const utils = trpc.useContext();
const { data: user, isLoading } = trpc.viewer.me.useQuery();
const { data: dataHasTeamPlan, isLoading: isLoadingHasTeamPlan } = trpc.viewer.teams.hasTeamPlan.useQuery();
const formMethods = useForm({
defaultValues: {
@ -73,7 +74,8 @@ const AppearanceView = () => {
},
});
if (isLoading) return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
if (isLoading || isLoadingHasTeamPlan)
return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
if (!user) return null;
@ -180,18 +182,18 @@ const AppearanceView = () => {
<p className="font-semibold ltr:mr-2 rtl:ml-2">
{t("disable_cal_branding", { appName: APP_NAME })}
</p>
<Badge variant="gray">{t("pro")}</Badge>
{!dataHasTeamPlan?.hasTeamPlan && <UpgradeTeamsBadge />}
</div>
<p className="mt-0.5 text-gray-600">{t("removes_cal_branding", { appName: APP_NAME })}</p>
</div>
<div className="flex-none">
<Switch
id="hideBranding"
disabled={!session.data?.user.belongsToActiveTeam}
disabled={!dataHasTeamPlan?.hasTeamPlan}
onCheckedChange={(checked) =>
formMethods.setValue("hideBranding", checked, { shouldDirty: true })
}
checked={!session.data?.user.belongsToActiveTeam ? false : value}
checked={!dataHasTeamPlan?.hasTeamPlan ? false : value}
/>
</div>
</div>

View File

@ -1509,5 +1509,6 @@
"using_meet_requires_calendar": "Using Google Meet requires a connected Google Calendar",
"continue_to_install_google_calendar": "Continue to install Google Calendar",
"install_google_meet": "Install Google Meet",
"install_google_calendar": "Install Google Calendar"
"install_google_calendar": "Install Google Calendar",
"no_recordings_found": "No recordings found"
}

View File

@ -7,7 +7,14 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RecordingItemSchema } from "@calcom/prisma/zod-utils";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import type { PartialReference } from "@calcom/types/EventManager";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui";
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
UpgradeTeamsBadge,
} from "@calcom/ui";
import { Button, showToast, Icon } from "@calcom/ui";
import RecordingListSkeleton from "./components/RecordingListSkeleton";
@ -58,9 +65,7 @@ export const ViewRecordingsDialog = (props: IViewRecordingsDialog) => {
const { t, i18n } = useLocale();
const { isOpenDialog, setIsOpenDialog, booking, timeFormat } = props;
const [downloadingRecordingId, setRecordingId] = useState<string | null>(null);
const session = useSession();
const belongsToActiveTeam = session?.data?.user?.belongsToActiveTeam ?? false;
const [showUpgradeBanner, setShowUpgradeBanner] = useState<boolean>(false);
const { data: dataHasTeamPlan, isLoading: isLoadingHasTeamPlan } = trpc.viewer.teams.hasTeamPlan.useQuery();
const roomName =
booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ??
undefined;
@ -103,57 +108,48 @@ export const ViewRecordingsDialog = (props: IViewRecordingsDialog) => {
<DialogContent>
<DialogHeader title={t("recordings_title")} subtitle={subtitle} />
<LicenseRequired>
{showUpgradeBanner && <UpgradeRecordingBanner />}
{!showUpgradeBanner && (
<>
{isLoading && <RecordingListSkeleton />}
{recordings && "data" in recordings && recordings?.data?.length > 0 && (
<div className="flex flex-col gap-3">
{recordings.data.map((recording: RecordingItemSchema, index: number) => {
return (
<div
className="flex w-full items-center justify-between rounded-md border py-2 px-4"
key={recording.id}>
<div className="flex flex-col">
<h1 className="text-sm font-semibold">
{t("recording")} {index + 1}
</h1>
<p className="text-sm font-normal text-gray-500">
{convertSecondsToMs(recording.duration)}
</p>
</div>
{belongsToActiveTeam ? (
<Button
StartIcon={Icon.FiDownload}
className="ml-4 lg:ml-0"
loading={downloadingRecordingId === recording.id}
onClick={() => handleDownloadClick(recording.id)}>
{t("download")}
</Button>
) : (
<Button
color="secondary"
tooltip={t("recordings_are_part_of_the_teams_plan")}
className="ml-4 lg:ml-0"
onClick={() => setShowUpgradeBanner(true)}>
{t("upgrade")}
</Button>
)}
<>
{isLoading && isLoadingHasTeamPlan && <RecordingListSkeleton />}
{recordings && "data" in recordings && recordings?.data?.length > 0 && (
<div className="flex flex-col gap-3">
{recordings.data.map((recording: RecordingItemSchema, index: number) => {
return (
<div
className="flex w-full items-center justify-between rounded-md border px-4 py-2"
key={recording.id}>
<div className="flex flex-col">
<h1 className="text-sm font-semibold">
{t("recording")} {index + 1}
</h1>
<p className="text-sm font-normal text-gray-500">
{convertSecondsToMs(recording.duration)}
</p>
</div>
);
})}
</div>
{dataHasTeamPlan?.hasTeamPlan ? (
<Button
StartIcon={Icon.FiDownload}
className="ml-4 lg:ml-0"
loading={downloadingRecordingId === recording.id}
onClick={() => handleDownloadClick(recording.id)}>
{t("download")}
</Button>
) : (
<UpgradeTeamsBadge />
)}
</div>
);
})}
</div>
)}
{!isLoading &&
(!recordings ||
(recordings && "total_count" in recordings && recordings?.total_count === 0)) && (
<h1 className="font-semibold">{t("no_recordings_found")}</h1>
)}
{!isLoading &&
(!recordings ||
(recordings && "total_count" in recordings && recordings?.total_count === 0)) && (
<h1 className="font-semibold">No Recordings Found</h1>
)}
</>
)}
</>
</LicenseRequired>
<DialogFooter>
<DialogClose onClick={() => setShowUpgradeBanner(false)} className="border" />
<DialogClose className="border" />
</DialogFooter>
</DialogContent>
</Dialog>

View File

@ -0,0 +1,18 @@
import Link from "next/link";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Tooltip } from "../tooltip";
import { Badge } from "./Badge";
export const UpgradeTeamsBadge = function UpgradeTeamsBadge() {
const { t } = useLocale();
return (
<Tooltip content={t("upgrade_to_enable_feature")}>
<Link href="/teams">
<Badge variant="gray">{t("upgrade")}</Badge>
</Link>
</Tooltip>
);
};

View File

@ -1,2 +1,4 @@
export { Badge } from "./Badge";
export { UpgradeTeamsBadge } from "./UpgradeTeamsBadge";
export type { BadgeProps } from "./Badge";

View File

@ -1,4 +1,3 @@
import { useRouter } from "next/router";
import {
components as reactSelectComponents,
ControlProps,
@ -13,11 +12,9 @@ import {
} from "react-select";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "../../../components/icon";
import { Badge } from "../../badge";
import { Tooltip } from "../../tooltip";
import { UpgradeTeamsBadge } from "../../badge";
export const InputComponent = <
Option,
@ -53,9 +50,6 @@ export const OptionComponent = <
className,
...props
}: OptionProps<Option, IsMulti, Group>) => {
const { t } = useLocale();
const router = useRouter();
return (
<reactSelectComponents.Option
{...props}
@ -67,13 +61,7 @@ export const OptionComponent = <
)}>
<>
<span className="mr-auto">{props.label}</span>
{(props.data as unknown as ExtendedOption).needsUpgrade && (
<Tooltip content={t("upgrade_to_enable_feature")}>
<button type="button" onClick={() => router.replace("/teams")}>
<Badge variant="gray">{t("upgrade")}</Badge>
</button>
</Tooltip>
)}
{(props.data as unknown as ExtendedOption).needsUpgrade && <UpgradeTeamsBadge />}
{props.isSelected && <Icon.FiCheck className="ml-2 h-4 w-4" />}
</>
</reactSelectComponents.Option>

View File

@ -1,6 +1,6 @@
export { Avatar, AvatarGroup } from "./avatar";
export type { AvatarProps, AvatarGroupProps } from "./avatar";
export { Badge } from "./badge";
export { Badge, UpgradeTeamsBadge } from "./badge";
export type { BadgeProps } from "./badge";
export { Breadcrumb, BreadcrumbContainer, BreadcrumbItem } from "./breadcrumb";
export { Button, LinkIconButton } from "./button";

View File

@ -2,6 +2,7 @@ export {
Avatar,
AvatarGroup,
Badge,
UpgradeTeamsBadge,
Breadcrumb,
BreadcrumbContainer,
BreadcrumbItem,