generic <UpgradeScreen> component (#6594)

* first attempt of <UpgradeScreen>

* changes to icons

* reverted changes back to initial state, needs fix: teams not showing

* WIP

* Fix weird reactnode error

* Fix loading text

* added upgradeTip to routing forms

* icon colors

* create and use hook to check if user has team plan

* use useTeamPlan for upgradeTeamsBadge

* replace huge svg with compressed jpeg

* responsive fixes

* Update packages/ui/components/badge/UpgradeTeamsBadge.tsx

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

* Give team plan features to E2E tests

* Allow option to make a user part of team int ests

* Remove flash of paywall for team user

* Add team user for typeform tests as well

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
This commit is contained in:
sean-brydon 2023-01-23 09:58:41 +00:00 committed by GitHub
parent 9602ac29fb
commit 1d792a2c6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 379 additions and 204 deletions

View File

@ -4,6 +4,7 @@ import { Controller, useForm } from "react-hook-form";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { APP_NAME } from "@calcom/lib/constants";
import { useHasTeamPlan } from "@calcom/lib/hooks/useHasTeamPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
@ -49,7 +50,8 @@ 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 { isLoading: isTeamPlanStatusLoading, hasTeamPlan } = useHasTeamPlan();
const formMethods = useForm({
defaultValues: {
@ -74,7 +76,7 @@ const AppearanceView = () => {
},
});
if (isLoading || isLoadingHasTeamPlan)
if (isLoading || isTeamPlanStatusLoading)
return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
if (!user) return null;
@ -182,18 +184,18 @@ const AppearanceView = () => {
<p className="font-semibold ltr:mr-2 rtl:ml-2">
{t("disable_cal_branding", { appName: APP_NAME })}
</p>
{!dataHasTeamPlan?.hasTeamPlan && <UpgradeTeamsBadge />}
<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={!dataHasTeamPlan?.hasTeamPlan}
disabled={!hasTeamPlan}
onCheckedChange={(checked) =>
formMethods.setValue("hideBranding", checked, { shouldDirty: true })
}
checked={!dataHasTeamPlan?.hasTeamPlan ? false : value}
checked={hasTeamPlan ? value : false}
/>
</div>
</div>

View File

@ -1,6 +1,6 @@
import type { Page, WorkerInfo } from "@playwright/test";
import type Prisma from "@prisma/client";
import { Prisma as PrismaType } from "@prisma/client";
import { Prisma as PrismaType, MembershipRole } from "@prisma/client";
import { hash } from "bcryptjs";
import dayjs from "@calcom/dayjs";
@ -34,6 +34,27 @@ const seededForm = {
type UserWithIncludes = PrismaType.UserGetPayload<typeof userWithEventTypes>;
const createTeamAndAddUser = async ({ user }: { user: { id: number; role?: MembershipRole } }) => {
const team = await prisma.team.create({
data: {
name: "",
slug: `team-${Date.now()}`,
},
});
if (!team) {
return;
}
const { role = MembershipRole.OWNER, id: userId } = user;
await prisma.membership.create({
data: {
teamId: team.id,
userId,
role: role,
},
});
};
// creates a user fixture instance and stores the collection
export const createUsersFixture = (page: Page, workerInfo: WorkerInfo) => {
const store = { users: [], page } as { users: UserFixture[]; page: typeof page };
@ -42,6 +63,7 @@ export const createUsersFixture = (page: Page, workerInfo: WorkerInfo) => {
opts?: CustomUserOpts | null,
scenario: {
seedRoutingForms?: boolean;
hasTeam?: true;
} = {}
) => {
const _user = await prisma.user.create({
@ -193,6 +215,9 @@ export const createUsersFixture = (page: Page, workerInfo: WorkerInfo) => {
where: { id: _user.id },
include: userIncludes,
});
if (scenario.hasTeam) {
await createTeamAndAddUser({ user: { id: user.id, role: "OWNER" } });
}
const userFixture = createUserFixture(user, store.page!);
store.users.push(userFixture);
return userFixture;

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1238,6 +1238,7 @@
"to": "To",
"workflow_turned_on_successfully": "{{workflowName}} workflow turned {{offOn}} successfully",
"download_responses": "Download Responses",
"download_responses_description": "Download all responses to your form in CSV format.",
"download": "Download",
"create_your_first_form": "Create your first form",
"create_your_first_form_description": "With Routing Forms you can ask qualifying questions and route to the correct person or event type.",
@ -1280,6 +1281,8 @@
"routing_forms_send_email_owner": "Send Email to Owner",
"routing_forms_send_email_owner_description": "Sends an email to the owner when the form is submitted",
"add_new_form": "Add new form",
"create_your_first_route": "Create your first route",
"route_to_the_right_person": "Route to the right person based on the answers to your form",
"form_description": "Create your form to route a booker",
"copy_link_to_form": "Copy link to form",
"theme": "Theme",
@ -1514,5 +1517,9 @@
"install_google_meet": "Install Google Meet",
"install_google_calendar": "Install Google Calendar",
"sender_name": "Sender name",
"no_recordings_found": "No recordings found"
"no_recordings_found": "No recordings found",
"reporting": "Reporting",
"reporting_feature": "See all incoming from data and download it as a CSV",
"teams_plan_required": "Teams plan required",
"routing_forms_are_a_great_way": "Routing forms are a great way to route your incoming leads to the right person. Upgrade to a Teams plan to access this feature."
}

View File

@ -1,8 +1,13 @@
// TODO: i18n
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { useMemo } from "react";
import SkeletonLoaderTeamList from "@calcom/features/ee/teams/components/SkeletonloaderTeamList";
import Shell, { ShellMain } from "@calcom/features/shell/Shell";
import { UpgradeTip } from "@calcom/features/tips";
import { WEBAPP_URL } from "@calcom/lib/constants";
import useApp from "@calcom/lib/hooks/useApp";
import { useHasTeamPlan } from "@calcom/lib/hooks/useHasTeamPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { AppGetServerSidePropsContext, AppPrisma, AppUser } from "@calcom/types/AppGetServerSideProps";
@ -15,6 +20,7 @@ import {
List,
ListLinkItem,
Tooltip,
Button,
} from "@calcom/ui";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@ -27,12 +33,47 @@ export default function RoutingForms({
appUrl,
}: inferSSRProps<typeof getServerSideProps> & { appUrl: string }) {
const { t } = useLocale();
const { data: forms } = trpc.viewer.appRoutingForms.forms.useQuery(undefined, {
const { hasTeamPlan } = useHasTeamPlan();
const { data: forms, isLoading } = trpc.viewer.appRoutingForms.forms.useQuery(undefined, {
initialData: forms_,
});
const { data: typeformApp } = useApp("typeform");
const features = [
{
icon: <Icon.FiFileText className="h-5 w-5 text-orange-500" />,
title: t("create_your_first_form"),
description: t("create_your_first_form_description"),
},
{
icon: <Icon.FiShuffle className="h-5 w-5 text-lime-500" />,
title: t("create_your_first_route"),
description: t("route_to_the_right_person"),
},
{
icon: <Icon.FiBarChart className="h-5 w-5 text-blue-500" />,
title: t("reporting"),
description: t("reporting_feature"),
},
{
icon: <Icon.FiCheckCircle className="h-5 w-5 text-teal-500" />,
title: t("test_routing_form"),
description: t("test_preview_description"),
},
{
icon: <Icon.FiMail className="h-5 w-5 text-yellow-500" />,
title: t("routing_forms_send_email_owner"),
description: t("routing_forms_send_email_owner_description"),
},
{
icon: <Icon.FiDownload className="h-5 w-5 text-violet-500" />,
title: t("download_responses"),
description: t("download_responses_description"),
},
];
function NewFormButton() {
return (
<FormAction
@ -46,137 +87,164 @@ export default function RoutingForms({
);
}
return (
<ShellMain heading="Routing Forms" CTA={<NewFormButton />} subtitle={t("routing_forms_description")}>
<FormActionsProvider appUrl={appUrl}>
<div className="-mx-4 md:-mx-8">
<div className="mb-10 w-full px-4 pb-2 sm:px-6 md:px-8">
{!forms?.length ? (
<EmptyScreen
Icon={Icon.FiGitMerge}
headline={t("create_your_first_form")}
description={t("create_your_first_form_description")}
buttonRaw={<NewFormButton />}
/>
) : null}
{forms?.length ? (
<div className="mb-16 overflow-hidden bg-white">
<List data-testid="routing-forms-list">
{forms.map((form, index) => {
if (!form) {
return null;
}
<ShellMain
heading="Routing Forms"
CTA={hasTeamPlan && <NewFormButton />}
subtitle={t("routing_forms_description")}>
<UpgradeTip
dark
title={t("teams_plan_required")}
description={t("routing_forms_are_a_great_way")}
features={features}
background="/routing-form-banner-background.jpg"
isParentLoading={isLoading && <SkeletonLoaderTeamList />}
buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
<Button color="secondary" href={`${WEBAPP_URL}/settings/teams/new`}>
{t("upgrade")}
</Button>
<Button
color="minimal"
className="!bg-transparent text-white opacity-50 hover:opacity-100"
href="https://go.cal.com/teams-video"
target="_blank">
{t("learn_more")}
</Button>
</ButtonGroup>
</div>
}>
<FormActionsProvider appUrl={appUrl}>
<div className="-mx-4 md:-mx-8">
<div className="mb-10 w-full px-4 pb-2 sm:px-6 md:px-8">
{!forms?.length ? (
<EmptyScreen
Icon={Icon.FiGitMerge}
headline={t("create_your_first_form")}
description={t("create_your_first_form_description")}
buttonRaw={<NewFormButton />}
/>
) : null}
{forms?.length ? (
<div className="mb-16 overflow-hidden bg-white">
<List data-testid="routing-forms-list">
{forms.map((form, index) => {
if (!form) {
return null;
}
const description = form.description || "";
const disabled = form.disabled;
form.routes = form.routes || [];
const fields = form.fields || [];
return (
<ListLinkItem
key={index}
href={appUrl + "/form-edit/" + form.id}
heading={form.name}
disabled={disabled}
subHeading={description}
actions={
<>
<FormAction className="self-center" action="toggle" routingForm={form} />
<ButtonGroup combined>
<Tooltip content={t("preview")}>
const description = form.description || "";
const disabled = form.disabled;
form.routes = form.routes || [];
const fields = form.fields || [];
return (
<ListLinkItem
key={index}
href={appUrl + "/form-edit/" + form.id}
heading={form.name}
disabled={disabled}
subHeading={description}
actions={
<>
<FormAction className="self-center" action="toggle" routingForm={form} />
<ButtonGroup combined>
<Tooltip content={t("preview")}>
<FormAction
action="preview"
routingForm={form}
target="_blank"
StartIcon={Icon.FiExternalLink}
color="secondary"
variant="icon"
disabled={disabled}
/>
</Tooltip>
<FormAction
action="preview"
routingForm={form}
target="_blank"
StartIcon={Icon.FiExternalLink}
action="copyLink"
color="secondary"
variant="icon"
StartIcon={Icon.FiLink}
disabled={disabled}
tooltip={t("copy_link_to_form")}
/>
</Tooltip>
<FormAction
routingForm={form}
action="copyLink"
color="secondary"
variant="icon"
StartIcon={Icon.FiLink}
disabled={disabled}
tooltip={t("copy_link_to_form")}
/>
<FormActionsDropdown form={form}>
<FormAction
action="edit"
routingForm={form}
color="minimal"
className="!flex"
StartIcon={Icon.FiEdit}>
{t("edit")}
</FormAction>
<FormAction
action="download"
routingForm={form}
color="minimal"
StartIcon={Icon.FiDownload}>
{t("download_responses")}
</FormAction>
<FormAction
action="embed"
routingForm={form}
color="minimal"
className="w-full"
StartIcon={Icon.FiCode}>
{t("embed")}
</FormAction>
<FormAction
action="duplicate"
routingForm={form}
color="minimal"
className="w-full"
StartIcon={Icon.FiCopy}>
{t("duplicate")}
</FormAction>
{typeformApp?.isInstalled ? (
<FormActionsDropdown form={form}>
<FormAction
data-testid="copy-redirect-url"
action="edit"
routingForm={form}
action="copyRedirectUrl"
color="minimal"
type="button"
StartIcon={Icon.FiLink}>
{t("Copy Typeform Redirect Url")}
className="!flex"
StartIcon={Icon.FiEdit}>
{t("edit")}
</FormAction>
) : null}
<DropdownMenuSeparator />
<FormAction
action="_delete"
routingForm={form}
color="destructive"
className="w-full"
StartIcon={Icon.FiTrash}>
{t("delete")}
</FormAction>
</FormActionsDropdown>
</ButtonGroup>
</>
}>
<div className="flex flex-wrap gap-1">
<Badge variant="gray" StartIcon={Icon.FiMenu}>
{fields.length} {fields.length === 1 ? "field" : "fields"}
</Badge>
<Badge variant="gray" StartIcon={Icon.FiGitMerge}>
{form.routes.length} {form.routes.length === 1 ? "route" : "routes"}
</Badge>
<Badge variant="gray" StartIcon={Icon.FiMessageCircle}>
{form._count.responses} {form._count.responses === 1 ? "response" : "responses"}
</Badge>
</div>
</ListLinkItem>
);
})}
</List>
</div>
) : null}
<FormAction
action="download"
routingForm={form}
color="minimal"
StartIcon={Icon.FiDownload}>
{t("download_responses")}
</FormAction>
<FormAction
action="embed"
routingForm={form}
color="minimal"
className="w-full"
StartIcon={Icon.FiCode}>
{t("embed")}
</FormAction>
<FormAction
action="duplicate"
routingForm={form}
color="minimal"
className="w-full"
StartIcon={Icon.FiCopy}>
{t("duplicate")}
</FormAction>
{typeformApp?.isInstalled ? (
<FormAction
data-testid="copy-redirect-url"
routingForm={form}
action="copyRedirectUrl"
color="minimal"
type="button"
StartIcon={Icon.FiLink}>
{t("Copy Typeform Redirect Url")}
</FormAction>
) : null}
<DropdownMenuSeparator />
<FormAction
action="_delete"
routingForm={form}
color="destructive"
className="w-full"
StartIcon={Icon.FiTrash}>
{t("delete")}
</FormAction>
</FormActionsDropdown>
</ButtonGroup>
</>
}>
<div className="flex flex-wrap gap-1">
<Badge variant="gray" StartIcon={Icon.FiMenu}>
{fields.length} {fields.length === 1 ? "field" : "fields"}
</Badge>
<Badge variant="gray" StartIcon={Icon.FiGitMerge}>
{form.routes.length} {form.routes.length === 1 ? "route" : "routes"}
</Badge>
<Badge variant="gray" StartIcon={Icon.FiMessageCircle}>
{form._count.responses} {form._count.responses === 1 ? "response" : "responses"}
</Badge>
</div>
</ListLinkItem>
);
})}
</List>
</div>
) : null}
</div>
</div>
</div>
</FormActionsProvider>
</FormActionsProvider>
</UpgradeTip>
</ShellMain>
);
}

View File

@ -118,7 +118,12 @@ test.describe("Routing Forms", () => {
// TODO: How to install the app just once?
test.beforeEach(async ({ page, users }) => {
const user = await users.create({ username: "routing-forms" });
const user = await users.create(
{ username: "routing-forms" },
{
hasTeam: true,
}
);
await user.login();
// Install app
await page.goto(`/apps/routing-forms`);
@ -148,7 +153,10 @@ test.describe("Routing Forms", () => {
users: Fixtures["users"];
page: Page;
}) {
const user = await users.create({ username: "routing-forms" }, { seedRoutingForms: true });
const user = await users.create(
{ username: "routing-forms" },
{ seedRoutingForms: true, hasTeam: true }
);
await user.login();
// Install app
await page.goto(`/apps/routing-forms`);

View File

@ -8,7 +8,12 @@ import { CAL_URL } from "@calcom/lib/constants";
import { Fixtures, test } from "@calcom/web/playwright/lib/fixtures";
const installApps = async (page: Page, users: Fixtures["users"]) => {
const user = await users.create({ username: "routing-forms" });
const user = await users.create(
{ username: "routing-forms" },
{
hasTeam: true,
}
);
await user.login();
await page.goto(`/apps/routing-forms`);
await page.click('[data-testid="install-app-button"]');

View File

@ -3,10 +3,10 @@ import { useState } from "react";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import isCalcom from "@calcom/lib/isCalcom";
import { trpc } from "@calcom/trpc/react";
import { Alert, Button, ButtonGroup, EmptyScreen, Icon } from "@calcom/ui";
import { Alert, Button, ButtonGroup, Icon } from "@calcom/ui";
import { UpgradeTip } from "../../../tips";
import SkeletonLoaderTeamList from "./SkeletonloaderTeamList";
import TeamList from "./TeamList";
@ -21,36 +21,35 @@ export function TeamsListing() {
});
const teams = data?.filter((m) => m.accepted) || [];
const invites = data?.filter((m) => !m.accepted) || [];
const features = [
{
icon: <Icon.FiUsers className="h-5 w-5 text-gray-700" />,
icon: <Icon.FiUsers className="h-5 w-5 text-red-500" />,
title: t("collective_scheduling"),
description: t("make_it_easy_to_book"),
},
{
icon: <Icon.FiRefreshCcw className="h-5 w-5 text-gray-700" />,
icon: <Icon.FiRefreshCcw className="h-5 w-5 text-blue-500" />,
title: t("round_robin"),
description: t("find_the_best_person"),
},
{
icon: <Icon.FiUserPlus className="h-5 w-5 text-gray-700" />,
icon: <Icon.FiUserPlus className="h-5 w-5 text-green-500" />,
title: t("fixed_round_robin"),
description: t("add_one_fixed_attendee"),
},
{
icon: <Icon.FiMail className="h-5 w-5 text-gray-700" />,
icon: <Icon.FiMail className="h-5 w-5 text-orange-500" />,
title: t("sms_attendee_action"),
description: t("make_it_easy_to_book"),
},
{
icon: <Icon.FiVideo className="h-5 w-5 text-gray-700" />,
icon: <Icon.FiVideo className="h-5 w-5 text-purple-500" />,
title: "Cal Video" + " " + t("recordings_title"),
description: t("upgrade_to_access_recordings_description"),
},
{
icon: <Icon.FiEyeOff className="h-5 w-5 text-gray-700" />,
icon: <Icon.FiEyeOff className="h-5 w-5 text-indigo-500" />,
title: t("disable_cal_branding", { appName: APP_NAME }),
description: t("disable_cal_branding_description", { appName: APP_NAME }),
},
@ -59,66 +58,26 @@ export function TeamsListing() {
return (
<>
{!!errorMessage && <Alert severity="error" title={errorMessage} />}
{invites.length > 0 && (
<div className="mb-4">
<h1 className="mb-2 text-lg font-medium">{t("open_invitations")}</h1>
<TeamList teams={invites} />
</div>
)}
{isLoading && <SkeletonLoaderTeamList />}
{!teams.length && !isLoading && (
<>
{!isCalcom ? (
<div className="-mt-6 rtl:ml-4 md:rtl:ml-0">
<div
className="flex w-full justify-between overflow-hidden rounded-lg pt-4 pb-10 md:min-h-[295px] md:pt-10"
style={{
background: "url(/team-banner-background.jpg)",
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
}}>
<div className="mt-3 px-8 sm:px-14">
<h1 className="font-cal text-3xl">{t("calcom_is_better_with_team")}</h1>
<p className="my-4 max-w-sm text-gray-600">{t("add_your_team_members")}</p>
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
{t("create_team")}
</Button>
<Button color="secondary" href="https://go.cal.com/teams-video" target="_blank">
{t("learn_more")}
</Button>
</ButtonGroup>
</div>
</div>
</div>
<div className="mt-4 grid-cols-3 md:grid md:gap-4">
{features.map((feature) => (
<div
key={feature.title}
className="mb-4 min-h-[180px] w-full rounded-md bg-gray-50 p-8 md:mb-0">
{feature.icon}
<h2 className="font-cal mt-4 text-lg">{feature.title}</h2>
<p className="text-gray-700">{feature.description}</p>
</div>
))}
</div>
</div>
) : (
<EmptyScreen
Icon={Icon.FiUsers}
headline={t("no_teams")}
description={t("no_teams_description")}
buttonRaw={
<Button color="secondary" href={`${WEBAPP_URL}/settings/teams/new`}>
{t("create_team")}
</Button>
}
/>
)}
</>
)}
{teams.length > 0 && <TeamList teams={teams} />}
<UpgradeTip
title="calcom_is_better_with_team"
description="add_your_team_members"
features={features}
background="/team-banner-background.jpg"
isParentLoading={isLoading && <SkeletonLoaderTeamList />}
buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
{t("create_team")}
</Button>
<Button color="secondary" href="https://go.cal.com/teams-video" target="_blank">
{t("learn_more")}
</Button>
</ButtonGroup>
</div>
}>
<TeamList teams={teams} />
</UpgradeTip>
</>
);
}

View File

@ -1,8 +1,8 @@
import { useSession } from "next-auth/react";
import { useState } from "react";
import dayjs from "@calcom/dayjs";
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
import useHasTeamPlan from "@calcom/lib/hooks/useHasTeamPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RecordingItemSchema } from "@calcom/prisma/zod-utils";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
@ -18,7 +18,6 @@ import {
import { Button, showToast, Icon } from "@calcom/ui";
import RecordingListSkeleton from "./components/RecordingListSkeleton";
import UpgradeRecordingBanner from "./components/UpgradeRecordingBanner";
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
@ -65,7 +64,9 @@ export const ViewRecordingsDialog = (props: IViewRecordingsDialog) => {
const { t, i18n } = useLocale();
const { isOpenDialog, setIsOpenDialog, booking, timeFormat } = props;
const [downloadingRecordingId, setRecordingId] = useState<string | null>(null);
const { data: dataHasTeamPlan, isLoading: isLoadingHasTeamPlan } = trpc.viewer.teams.hasTeamPlan.useQuery();
const { hasTeamPlan, isLoading: isTeamPlanStatusLoading } = useHasTeamPlan();
const roomName =
booking?.references?.find((reference: PartialReference) => reference.type === "daily_video")?.meetingId ??
undefined;
@ -109,7 +110,7 @@ export const ViewRecordingsDialog = (props: IViewRecordingsDialog) => {
<DialogHeader title={t("recordings_title")} subtitle={subtitle} />
<LicenseRequired>
<>
{(isLoading || isLoadingHasTeamPlan) && <RecordingListSkeleton />}
{(isLoading || isTeamPlanStatusLoading) && <RecordingListSkeleton />}
{recordings && "data" in recordings && recordings?.data?.length > 0 && (
<div className="flex flex-col gap-3">
{recordings.data.map((recording: RecordingItemSchema, index: number) => {
@ -125,7 +126,7 @@ export const ViewRecordingsDialog = (props: IViewRecordingsDialog) => {
{convertSecondsToMs(recording.duration)}
</p>
</div>
{dataHasTeamPlan?.hasTeamPlan ? (
{hasTeamPlan ? (
<Button
StartIcon={Icon.FiDownload}
className="ml-4 lg:ml-0"

View File

@ -0,0 +1,86 @@
import { useMemo } from "react";
import type { ReactNode } from "react";
import { classNames } from "@calcom/lib";
import { useHasTeamPlan } from "@calcom/lib/hooks/useHasTeamPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
// import isCalcom from "@calcom/lib/isCalcom";
import { trpc } from "@calcom/trpc/react";
import { EmptyScreen, Icon } from "@calcom/ui";
import TeamList from "../ee/teams/components/TeamList";
const isCalcom = true;
export function UpgradeTip({
dark,
title,
description,
background,
features,
buttons,
isParentLoading,
children,
}: {
dark?: boolean;
title: string;
description: string;
background: string;
features: Array<{ icon: JSX.Element; title: string; description: string }>;
buttons?: JSX.Element;
/**Chldren renders when the user is in a team */
children: JSX.Element;
isParentLoading?: ReactNode;
}) {
const { data } = trpc.viewer.teams.list.useQuery();
const invites = useMemo(() => data?.filter((m) => !m.accepted) || [], [data]);
const { t } = useLocale();
const { isLoading, hasTeamPlan } = useHasTeamPlan();
if (hasTeamPlan) return children;
if (!isCalcom)
return <EmptyScreen Icon={Icon.FiUsers} headline={title} description={description} buttonRaw={buttons} />;
if (isParentLoading || isLoading) return <>{isParentLoading}</>;
return (
<>
<div className="-mt-10 rtl:ml-4 sm:mt-0 md:rtl:ml-0 lg:-mt-6">
<div
className="flex w-full justify-between overflow-hidden rounded-lg pt-4 pb-10 md:min-h-[295px] md:pt-10"
style={{
background: `url(${background})`,
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
}}>
<div className="mt-3 px-8 sm:px-14">
<h1 className={classNames("font-cal text-3xl", dark && "text-white")}>{t(title)}</h1>
<p className={classNames("my-4 max-w-sm", dark ? "text-white" : "text-gray-600")}>
{t(description)}
</p>
{buttons}
</div>
</div>
{invites.length > 0 && (
<div className="my-4">
<h3 className="font-cal mb-4 text-xl">{t("open_invitations")}</h3>
<TeamList teams={invites} />
</div>
)}
<div className="mt-4 grid-cols-3 md:grid md:gap-4">
{invites.length === 0 &&
features.map((feature) => (
<div
key={feature.title}
className="mb-4 min-h-[180px] w-full rounded-md bg-gray-50 p-8 md:mb-0">
{feature.icon}
<h2 className="font-cal mt-4 text-lg">{feature.title}</h2>
<p className="text-gray-700">{feature.description}</p>
</div>
))}
</div>
</div>
</>
);
}

View File

@ -1 +1,2 @@
export { default as Tips } from "./Tips";
export { UpgradeTip } from "./UpgradeTip";

View File

@ -0,0 +1,9 @@
import { trpc } from "@calcom/trpc/react";
export function useHasTeamPlan() {
const hasTeam = trpc.viewer.teams.hasTeamPlan.useQuery();
return { isLoading: hasTeam.isLoading, hasTeamPlan: hasTeam.data?.hasTeamPlan || false };
}
export default useHasTeamPlan;

View File

@ -1,5 +1,6 @@
import Link from "next/link";
import { useHasTeamPlan } from "@calcom/lib/hooks/useHasTeamPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Tooltip } from "../tooltip";
@ -7,6 +8,9 @@ import { Badge } from "./Badge";
export const UpgradeTeamsBadge = function UpgradeTeamsBadge() {
const { t } = useLocale();
const { hasTeamPlan } = useHasTeamPlan();
if (hasTeamPlan) return null;
return (
<Tooltip content={t("upgrade_to_enable_feature")}>