Fix/connected calendar component (#3956)

* Fix for displaying connected calendars alert with test

* Fix default calendar display string when externalId its too long

* Update apps/web/components/ui/LinkText.tsx

Co-authored-by: Omar López <zomars@me.com>

Co-authored-by: Omar López <zomars@me.com>
pull/3905/head^2
alannnc 2022-08-24 19:44:14 -06:00 committed by GitHub
parent 5950b62b02
commit 1b541ff214
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 177 additions and 73 deletions

View File

@ -70,7 +70,13 @@ const DestinationCalendarSelector = ({
value: `${cal.integration}:${cal.externalId}`,
})),
})) ?? [];
const defaultCalendarSelectedString = destinationCalendar?.externalId
? `(${
destinationCalendar.externalId.length > 15
? destinationCalendar.externalId.substring(0, 15) + "..."
: destinationCalendar.externalId
})`
: "";
return (
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
<Select
@ -79,8 +85,8 @@ const DestinationCalendarSelector = ({
!hidePlaceholder ? (
`${t("select_destination_calendar")}`
) : (
<span>
{t("default_calendar_selected")} ({destinationCalendar?.externalId})
<span className="whitespace-nowrap">
{t("default_calendar_selected")} {defaultCalendarSelectedString}
</span>
)
}
@ -104,7 +110,6 @@ const DestinationCalendarSelector = ({
borderRadius: "2px",
"@media only screen and (min-width: 640px)": {
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
maxWidth: "320px",
},
};
},

View File

@ -0,0 +1,19 @@
import Link from "next/link";
interface ILinkTextProps {
href: string;
children: React.ReactNode;
classNameChildren?: string;
}
/**
* This component had to be made in order to make i18n work with next/link
* @see https://github.com/i18next/react-i18next/issues/1090#issuecomment-615426145
**/
export const LinkText = (props: ILinkTextProps) => {
const { href, children, classNameChildren, ...moreProps } = props;
return (
<Link href={href || ""} {...moreProps}>
<a className={classNameChildren}>{children}</a>
</Link>
);
};

View File

@ -0,0 +1,40 @@
import { cleanup, screen, render } from "@testing-library/react";
import { trpc } from "@calcom/trpc/react";
import NoCalendarConnectedAlert from "..";
describe("Testing NoCalendarConnectedAlert", () => {
describe("Render test", () => {
it("should render without crashing", () => {
// Disabled as its asking for full trpc useQuery response
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
jest.spyOn(trpc, "useQuery").mockReturnValue({
isSuccess: true,
isFetched: true,
data: { connectedCalendars: [], destinationCalendar: undefined },
});
render(<NoCalendarConnectedAlert />);
const testProp = screen.getByText("missing_connected_calendar");
expect(testProp).toBeTruthy();
});
it("should not render", () => {
// Disabled as its asking for full trpc useQuery response
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
jest.spyOn(trpc, "useQuery").mockReturnValue({
isSuccess: true,
isFetched: true,
data: { connectedCalendars: [1, 2], destinationCalendar: { a: "test" } },
});
render(<NoCalendarConnectedAlert />);
expect(screen.queryByText("missing_connected_calendar")).toBeNull();
});
afterEach(cleanup);
});
});

View File

@ -0,0 +1,44 @@
import { useTranslation } from "next-i18next";
import { Trans } from "next-i18next";
import { trpc } from "@calcom/trpc/react";
// @TODO: alert v2
import { Alert } from "@calcom/ui/Alert";
import { LinkText } from "../LinkText";
const NoCalendarConnectedAlert = () => {
const { t } = useTranslation();
const query = trpc.useQuery(["viewer.connectedCalendars"]);
// We are not gonna show this alert till we fetch data from DB
let defaultCalendarConnected = true;
if (query.isSuccess && query.isFetched && query.data) {
defaultCalendarConnected =
query.data.connectedCalendars.length > 0 && query.data.destinationCalendar !== undefined;
}
return (
<>
{!defaultCalendarConnected && (
<Alert
severity="warning"
className="mb-4"
title={<>{t("missing_connected_calendar") as string}</>}
message={
<Trans i18nKey="connect_your_calendar_and_link">
You can connect your calendar from
<LinkText
data-testid="no-calendar-connected-alert"
href="/apps/categories/calendar"
classNameChildren="underline">
here
</LinkText>
.
</Trans>
}
/>
)}
</>
);
};
export default NoCalendarConnectedAlert;

View File

@ -7,7 +7,7 @@ const config: Config.InitialOptions = {
verbose: true,
roots: ["<rootDir>"],
setupFiles: ["<rootDir>/test/jest-setup.js"],
testMatch: ["**/test/lib/**/*.(spec|test).(ts|tsx|js)"],
testMatch: ["**/test/lib/**/*.(spec|test).(ts|tsx|js)", "**/__tests__/**/*.(spec|test).(ts|tsx|js)"],
testPathIgnorePatterns: ["<rootDir>/.next", "<rootDir>/playwright/"],
transform: {
"^.+\\.(js|jsx|ts|tsx)$": ["babel-jest", { presets: ["next/babel"] }],

View File

@ -128,6 +128,7 @@
"@calcom/types": "*",
"@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
"@playwright/test": "^1.25.0",
"@testing-library/react": "^13.3.0",
"@types/accept-language-parser": "1.5.2",
"@types/async": "^3.2.15",
"@types/bcryptjs": "^2.4.2",

View File

@ -1,5 +1,4 @@
import { UserPlan } from "@prisma/client";
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
import { Trans } from "next-i18next";
import Head from "next/head";
import Link from "next/link";
@ -9,7 +8,6 @@ import React, { Fragment, useEffect, useState } from "react";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import prisma from "@calcom/prisma";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
import { Alert } from "@calcom/ui/Alert";
@ -28,7 +26,6 @@ import Shell from "@calcom/ui/Shell";
import { Tooltip } from "@calcom/ui/Tooltip";
import { withQuery } from "@lib/QueryCell";
import { getSession } from "@lib/auth";
import classNames from "@lib/classNames";
import { HttpError } from "@lib/core/http/error";
@ -38,13 +35,13 @@ import EventTypeDescription from "@components/eventtype/EventTypeDescription";
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
import Avatar from "@components/ui/Avatar";
import AvatarGroup from "@components/ui/AvatarGroup";
import { LinkText } from "@components/ui/LinkText";
import NoCalendarConnectedAlert from "@components/ui/NoCalendarConnectedAlert";
import { TRPCClientError } from "@trpc/react";
type EventTypeGroups = inferQueryOutput<"viewer.eventTypes">["eventTypeGroups"];
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
type ConnectedCalendars = inferQueryOutput<"viewer.connectedCalendars">["connectedCalendars"][number];
interface EventTypeListHeadingProps {
profile: EventTypeGroupProfile;
membershipCount: number;
@ -59,17 +56,7 @@ interface EventTypeListProps {
types: EventType[];
}
const Item = ({
type,
group,
readOnly,
connectedCalendars,
}: {
type: EventType;
group: EventTypeGroup;
readOnly: boolean;
connectedCalendars: ConnectedCalendars[] | undefined;
}) => {
const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGroup; readOnly: boolean }) => {
const { t } = useLocale();
return (
@ -216,8 +203,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
}
}, []);
const connectedCalendarsQuery = trpc.useQuery(["viewer.connectedCalendars"]);
return (
<div className="-mx-4 mb-16 overflow-hidden rounded-sm border border-gray-200 bg-white sm:mx-0">
<ul className="divide-y divide-neutral-200" data-testid="event-types">
@ -254,12 +239,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
</button>
</>
)}
<MemoizedItem
type={type}
group={group}
readOnly={readOnly}
connectedCalendars={connectedCalendarsQuery.data?.connectedCalendars}
/>
<MemoizedItem type={type} group={group} readOnly={readOnly} />
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 sm:flex">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
{type.users?.length > 1 && (
@ -571,9 +551,8 @@ const CTA = () => {
const WithQuery = withQuery(["viewer.eventTypes"]);
const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideProps>) => {
const EventTypesPage = () => {
const { t } = useLocale();
const { defaultCalendarConnected } = props;
return (
<div>
@ -597,31 +576,17 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
message={
<Trans i18nKey="plan_upgrade_instructions">
You can
<a href="/api/upgrade" className="underline">
<LinkText href="/api/upgrade" classNameChildren="underline">
upgrade here
</a>
</LinkText>
.
</Trans>
}
className="mb-4"
/>
)}
{!defaultCalendarConnected && (
<Alert
severity="warning"
className="mb-4"
title={<>{t("missing_connected_calendar") as string}</>}
message={
<Trans i18nKey="connect_your_calendar_and_link">
You can connect your calendar from
<a href="/apps/categories/calendar" className="underline">
here
</a>
.
</Trans>
}
/>
)}
<NoCalendarConnectedAlert />
{data.eventTypeGroups.map((group, index) => (
<Fragment key={group.profile.slug}>
@ -652,26 +617,4 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
);
};
export async function getServerSideProps(context: GetServerSidePropsContext) {
const session = await getSession(context);
let defaultCalendarConnected = false;
if (session && session.user) {
const defaultCalendar = await prisma.destinationCalendar.findFirst({
where: {
userId: session.user.id,
credentialId: {
not: null,
},
},
});
defaultCalendarConnected = !!defaultCalendar;
}
return {
props: {
defaultCalendarConnected,
},
};
}
export default EventTypesPage;

View File

@ -5687,6 +5687,29 @@
lodash.isplainobject "^4.0.6"
lodash.merge "^4.6.2"
"@testing-library/dom@^8.5.0":
version "8.17.1"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.17.1.tgz#2d7af4ff6dad8d837630fecd08835aee08320ad7"
integrity sha512-KnH2MnJUzmFNPW6RIKfd+zf2Wue8mEKX0M3cpX6aKl5ZXrJM1/c/Pc8c2xDNYQCnJO48Sm5ITbMXgqTr3h4jxQ==
dependencies:
"@babel/code-frame" "^7.10.4"
"@babel/runtime" "^7.12.5"
"@types/aria-query" "^4.2.0"
aria-query "^5.0.0"
chalk "^4.1.0"
dom-accessibility-api "^0.5.9"
lz-string "^1.4.4"
pretty-format "^27.0.2"
"@testing-library/react@^13.3.0":
version "13.3.0"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.3.0.tgz#bf298bfbc5589326bbcc8052b211f3bb097a97c5"
integrity sha512-DB79aA426+deFgGSjnf5grczDPiL4taK3hFaa+M5q7q20Kcve9eQottOG5kZ74KEr55v0tU2CQormSSDK87zYQ==
dependencies:
"@babel/runtime" "^7.12.5"
"@testing-library/dom" "^8.5.0"
"@types/react-dom" "^18.0.0"
"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@ -5803,6 +5826,11 @@
dependencies:
"@types/estree" "*"
"@types/aria-query@^4.2.0":
version "4.2.2"
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc"
integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==
"@types/asn1js@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@types/asn1js/-/asn1js-2.0.2.tgz#bb1992291381b5f06e22a829f2ae009267cdf8c5"
@ -6314,7 +6342,7 @@
dependencies:
"@types/react" "*"
"@types/react-dom@18.0.4":
"@types/react-dom@18.0.4", "@types/react-dom@^18.0.0":
version "18.0.4"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.4.tgz#dcbcadb277bcf6c411ceff70069424c57797d375"
integrity sha512-FgTtbqPOCI3dzZPZoC2T/sx3L34qxy99ITWn4eoSA95qPyXDMH0ALoAqUp49ITniiJFsXUVBtalh/KffMpg21Q==
@ -7370,6 +7398,11 @@ aria-query@^4.2.2:
"@babel/runtime" "^7.10.2"
"@babel/runtime-corejs3" "^7.10.2"
aria-query@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c"
integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==
arr-diff@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@ -10136,6 +10169,11 @@ doctrine@^2.1.0:
dependencies:
esutils "^2.0.2"
dom-accessibility-api@^0.5.9:
version "0.5.14"
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz#56082f71b1dc7aac69d83c4285eef39c15d93f56"
integrity sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==
dom-converter@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
@ -15620,6 +15658,11 @@ luxon@^1.26.0:
resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf"
integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==
lz-string@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==
magic-string@^0.26.1, magic-string@^0.26.2:
version "0.26.2"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.2.tgz#5331700e4158cd6befda738bb6b0c7b93c0d4432"
@ -18626,6 +18669,15 @@ pretty-error@^4.0.0:
lodash "^4.17.20"
renderkid "^3.0.0"
pretty-format@^27.0.2:
version "27.5.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
dependencies:
ansi-regex "^5.0.1"
ansi-styles "^5.0.0"
react-is "^17.0.1"
pretty-format@^28.0.0, pretty-format@^28.1.3:
version "28.1.3"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.3.tgz#c9fba8cedf99ce50963a11b27d982a9ae90970d5"
@ -19286,7 +19338,7 @@ react-intl@^5.25.1:
intl-messageformat "9.13.0"
tslib "^2.1.0"
react-is@17.0.2, react-is@^17.0.2:
react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==