diff --git a/apps/web/pages/settings/my-account/appearance.tsx b/apps/web/pages/settings/my-account/appearance.tsx index a0056343ba..7a535318b1 100644 --- a/apps/web/pages/settings/my-account/appearance.tsx +++ b/apps/web/pages/settings/my-account/appearance.tsx @@ -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 ; if (!user) return null; @@ -182,18 +184,18 @@ const AppearanceView = () => {

{t("disable_cal_branding", { appName: APP_NAME })}

- {!dataHasTeamPlan?.hasTeamPlan && } +

{t("removes_cal_branding", { appName: APP_NAME })}

formMethods.setValue("hideBranding", checked, { shouldDirty: true }) } - checked={!dataHasTeamPlan?.hasTeamPlan ? false : value} + checked={hasTeamPlan ? value : false} />
diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index 84128d6f96..3fdb9f4a4a 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -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; +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; diff --git a/apps/web/public/routing-form-banner-background.jpg b/apps/web/public/routing-form-banner-background.jpg new file mode 100644 index 0000000000..ba472f65fb Binary files /dev/null and b/apps/web/public/routing-form-banner-background.jpg differ diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 7586f9f14f..51a7c408c3 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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." } diff --git a/packages/app-store/ee/routing-forms/pages/forms/[...appPages].tsx b/packages/app-store/ee/routing-forms/pages/forms/[...appPages].tsx index c63f6098e4..198fddf8e8 100644 --- a/packages/app-store/ee/routing-forms/pages/forms/[...appPages].tsx +++ b/packages/app-store/ee/routing-forms/pages/forms/[...appPages].tsx @@ -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 & { 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: , + title: t("create_your_first_form"), + description: t("create_your_first_form_description"), + }, + { + icon: , + title: t("create_your_first_route"), + description: t("route_to_the_right_person"), + }, + { + icon: , + title: t("reporting"), + description: t("reporting_feature"), + }, + { + icon: , + title: t("test_routing_form"), + description: t("test_preview_description"), + }, + { + icon: , + title: t("routing_forms_send_email_owner"), + description: t("routing_forms_send_email_owner_description"), + }, + { + icon: , + title: t("download_responses"), + description: t("download_responses_description"), + }, + ]; + function NewFormButton() { return ( } subtitle={t("routing_forms_description")}> - -
-
- {!forms?.length ? ( - } - /> - ) : null} - {forms?.length ? ( -
- - {forms.map((form, index) => { - if (!form) { - return null; - } + } + subtitle={t("routing_forms_description")}> + } + buttons={ +
+ + + + +
+ }> + +
+
+ {!forms?.length ? ( + } + /> + ) : null} + {forms?.length ? ( +
+ + {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 ( - - - - + const description = form.description || ""; + const disabled = form.disabled; + form.routes = form.routes || []; + const fields = form.fields || []; + return ( + + + + + + - - - - - {t("edit")} - - - {t("download_responses")} - - - {t("embed")} - - - {t("duplicate")} - - {typeformApp?.isInstalled ? ( + - {t("Copy Typeform Redirect Url")} + className="!flex" + StartIcon={Icon.FiEdit}> + {t("edit")} - ) : null} - - - {t("delete")} - - - - - }> -
- - {fields.length} {fields.length === 1 ? "field" : "fields"} - - - {form.routes.length} {form.routes.length === 1 ? "route" : "routes"} - - - {form._count.responses} {form._count.responses === 1 ? "response" : "responses"} - -
-
- ); - })} -
-
- ) : null} + + {t("download_responses")} + + + {t("embed")} + + + {t("duplicate")} + + {typeformApp?.isInstalled ? ( + + {t("Copy Typeform Redirect Url")} + + ) : null} + + + {t("delete")} + + + + + }> +
+ + {fields.length} {fields.length === 1 ? "field" : "fields"} + + + {form.routes.length} {form.routes.length === 1 ? "route" : "routes"} + + + {form._count.responses} {form._count.responses === 1 ? "response" : "responses"} + +
+ + ); + })} + +
+ ) : null} +
-
- + + ); } diff --git a/packages/app-store/ee/routing-forms/playwright/tests/basic.e2e.ts b/packages/app-store/ee/routing-forms/playwright/tests/basic.e2e.ts index 57ed3be2c8..e413277403 100644 --- a/packages/app-store/ee/routing-forms/playwright/tests/basic.e2e.ts +++ b/packages/app-store/ee/routing-forms/playwright/tests/basic.e2e.ts @@ -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`); diff --git a/packages/app-store/typeform/playwright/tests/basic.e2e.ts b/packages/app-store/typeform/playwright/tests/basic.e2e.ts index d8d74147e0..ce52acb620 100644 --- a/packages/app-store/typeform/playwright/tests/basic.e2e.ts +++ b/packages/app-store/typeform/playwright/tests/basic.e2e.ts @@ -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"]'); diff --git a/packages/features/ee/teams/components/TeamsListing.tsx b/packages/features/ee/teams/components/TeamsListing.tsx index 57fd3356b4..43fa44cadc 100644 --- a/packages/features/ee/teams/components/TeamsListing.tsx +++ b/packages/features/ee/teams/components/TeamsListing.tsx @@ -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: , title: t("collective_scheduling"), description: t("make_it_easy_to_book"), }, { - icon: , + icon: , title: t("round_robin"), description: t("find_the_best_person"), }, { - icon: , + icon: , title: t("fixed_round_robin"), description: t("add_one_fixed_attendee"), }, { - icon: , + icon: , title: t("sms_attendee_action"), description: t("make_it_easy_to_book"), }, { - icon: , + icon: , title: "Cal Video" + " " + t("recordings_title"), description: t("upgrade_to_access_recordings_description"), }, { - icon: , + icon: , 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 && } - {invites.length > 0 && ( -
-

{t("open_invitations")}

- -
- )} - {isLoading && } - {!teams.length && !isLoading && ( - <> - {!isCalcom ? ( -
-
-
-

{t("calcom_is_better_with_team")}

-

{t("add_your_team_members")}

-
- - - - -
-
-
-
- {features.map((feature) => ( -
- {feature.icon} -

{feature.title}

-

{feature.description}

-
- ))} -
-
- ) : ( - - {t("create_team")} - - } - /> - )} - - )} - {teams.length > 0 && } + } + buttons={ +
+ + + + +
+ }> + +
); } diff --git a/packages/features/ee/video/ViewRecordingsDialog.tsx b/packages/features/ee/video/ViewRecordingsDialog.tsx index 0b46197c43..96343263af 100644 --- a/packages/features/ee/video/ViewRecordingsDialog.tsx +++ b/packages/features/ee/video/ViewRecordingsDialog.tsx @@ -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(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) => { <> - {(isLoading || isLoadingHasTeamPlan) && } + {(isLoading || isTeamPlanStatusLoading) && } {recordings && "data" in recordings && recordings?.data?.length > 0 && (
{recordings.data.map((recording: RecordingItemSchema, index: number) => { @@ -125,7 +126,7 @@ export const ViewRecordingsDialog = (props: IViewRecordingsDialog) => { {convertSecondsToMs(recording.duration)}

- {dataHasTeamPlan?.hasTeamPlan ? ( + {hasTeamPlan ? (